diff --git a/cmd/cobra.go b/cmd/cobra.go new file mode 100644 index 0000000..e89929a --- /dev/null +++ b/cmd/cobra.go @@ -0,0 +1,110 @@ +package xcaddycmd + +import ( + "fmt" + "log" + "os" + "os/exec" + + "github.com/caddyserver/xcaddy" + "github.com/caddyserver/xcaddy/internal/utils" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "xcaddy", + Long: "", + SilenceUsage: true, + Version: xcaddyVersion(), + RunE: func(cmd *cobra.Command, args []string) error { + binOutput := getCaddyOutputFile() + + // get current/main module name and the root directory of the main module + // + // make sure the module being developed is replaced + // so that the local copy is used + // + // replace directives only apply to the top-level/main go.mod, + // and since this tool is a carry-through for the user's actual + // go.mod, we need to transfer their replace directives through + // to the one we're making + execCmd := exec.Command(utils.GetGo(), "list", "-mod=readonly", "-m", "-json", "all") + execCmd.Stderr = os.Stderr + out, err := execCmd.Output() + if err != nil { + return fmt.Errorf("exec %v: %v: %s", cmd.Args, err, string(out)) + } + currentModule, moduleDir, replacements, err := parseGoListJson(out) + if err != nil { + return fmt.Errorf("json parse error: %v", err) + } + + // reconcile remaining path segments; for example if a module foo/a + // is rooted at directory path /home/foo/a, but the current directory + // is /home/foo/a/b, then the package to import should be foo/a/b + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("unable to determine current directory: %v", err) + } + importPath := normalizeImportPath(currentModule, cwd, moduleDir) + + // build caddy with this module plugged in + builder := xcaddy.Builder{ + Compile: xcaddy.Compile{ + Cgo: os.Getenv("CGO_ENABLED") == "1", + }, + CaddyVersion: caddyVersion, + Plugins: []xcaddy.Dependency{ + {PackagePath: importPath}, + }, + Replacements: replacements, + RaceDetector: raceDetector, + SkipBuild: skipBuild, + SkipCleanup: skipCleanup, + Debug: buildDebugOutput, + } + err = builder.Build(cmd.Context(), binOutput) + if err != nil { + return err + } + + // if requested, run setcap to allow binding to low ports + err = setcapIfRequested(binOutput) + if err != nil { + return err + } + + log.Printf("[INFO] Running %v\n\n", append([]string{binOutput}, args...)) + + execCmd = exec.Command(binOutput, args...) + execCmd.Stdin = os.Stdin + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + err = execCmd.Start() + if err != nil { + return err + } + defer func() { + if skipCleanup { + log.Printf("[INFO] Skipping cleanup as requested; leaving artifact: %s", binOutput) + return + } + err = os.Remove(binOutput) + if err != nil && !os.IsNotExist(err) { + log.Printf("[ERROR] Deleting temporary binary %s: %v", binOutput, err) + } + }() + + return execCmd.Wait() + }, +} + +const fullDocsFooter = `Full documentation is available at: +https://github.com/caddyserver/xcaddy` + +func init() { + rootCmd.SetVersionTemplate("{{.Version}}\n") + rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") + rootCmd.AddCommand(buildCommand) + rootCmd.AddCommand(versionCommand) +} diff --git a/cmd/commands.go b/cmd/commands.go new file mode 100644 index 0000000..ffcc16d --- /dev/null +++ b/cmd/commands.go @@ -0,0 +1,172 @@ +package xcaddycmd + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/caddyserver/xcaddy" + "github.com/caddyserver/xcaddy/internal/utils" + "github.com/spf13/cobra" +) + +func init() { + buildCommand.Flags().StringArray("with", []string{}, "can be used multiple times to add plugins by specifying the Go module name and optionally its version, similar to go get. Module name is required, but specific version and/or local replacement are optional.") + buildCommand.Flags().String("output", "", "changes the output file.") + buildCommand.Flags().StringArray("replace", []string{}, "is like --with, but does not add a blank import to the code; it only writes a replace directive to go.mod, which is useful when developing on Caddy's dependencies (ones that are not Caddy modules). Try this if you got an error when using --with, like \"cannot find module providing package\".") + buildCommand.Flags().StringArray("embed", []string{}, "can be used multiple times to embed directories into the built Caddy executable. The directory can be prefixed with a custom alias and a colon : to use it with the root directive and sub-directive.") +} + +var versionCommand = &cobra.Command{ + Use: "version", + Long: "Getting xcaddy version", + RunE: func(cm *cobra.Command, args []string) error { + fmt.Println(xcaddyVersion()) + return nil + }, +} + +var buildCommand = &cobra.Command{ + Use: `build [] + [--output ] + [--with ...] + [--replace ...] + [--embed <[alias]:path/to/dir>...]`, + Long: "", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var output string + var plugins []xcaddy.Dependency + var replacements []xcaddy.Replace + var embedDir []string + var argCaddyVersion string + if len(args) > 0 { + argCaddyVersion = args[0] + } + withArgs, err := cmd.Flags().GetStringArray("with") + if err != nil { + return fmt.Errorf("unable to parse --with arguments: %s", err.Error()) + } + + replaceArgs, err := cmd.Flags().GetStringArray("replace") + if err != nil { + return fmt.Errorf("unable to parse --replace arguments: %s", err.Error()) + } + for _, withArg := range withArgs { + mod, ver, _, err := splitWith(withArg) + if err != nil { + return err + } + mod = strings.TrimSuffix(mod, "/") // easy to accidentally leave a trailing slash if pasting from a URL, but is invalid for Go modules + plugins = append(plugins, xcaddy.Dependency{ + PackagePath: mod, + Version: ver, + }) + } + + for _, withArg := range replaceArgs { + mod, ver, repl, err := splitWith(withArg) + if err != nil { + return err + } + // adjust relative replacements in current working directory since our temporary module is in a different directory + if strings.HasPrefix(repl, ".") { + repl, err = filepath.Abs(repl) + if err != nil { + log.Fatalf("[FATAL] %v", err) + } + log.Printf("[INFO] Resolved relative replacement %s to %s", withArg, repl) + } + replacements = append(replacements, xcaddy.NewReplace(xcaddy.Dependency{PackagePath: mod, Version: ver}.String(), repl)) + } + + output, err = cmd.Flags().GetString("output") + if err != nil { + return fmt.Errorf("unable to parse --output arguments: %s", err.Error()) + } + + embedDir, err = cmd.Flags().GetStringArray("embed") + if err != nil { + return fmt.Errorf("unable to parse --embed arguments: %s", err.Error()) + } + // prefer caddy version from command line argument over env var + if argCaddyVersion != "" { + caddyVersion = argCaddyVersion + } + + // ensure an output file is always specified + if output == "" { + output = getCaddyOutputFile() + } + + // perform the build + builder := xcaddy.Builder{ + Compile: xcaddy.Compile{ + Cgo: os.Getenv("CGO_ENABLED") == "1", + }, + CaddyVersion: caddyVersion, + Plugins: plugins, + Replacements: replacements, + RaceDetector: raceDetector, + SkipBuild: skipBuild, + SkipCleanup: skipCleanup, + Debug: buildDebugOutput, + BuildFlags: buildFlags, + ModFlags: modFlags, + } + for _, md := range embedDir { + if before, after, found := strings.Cut(md, ":"); found { + builder.EmbedDirs = append(builder.EmbedDirs, struct { + Dir string `json:"dir,omitempty"` + Name string `json:"name,omitempty"` + }{ + after, before, + }) + } else { + builder.EmbedDirs = append(builder.EmbedDirs, struct { + Dir string `json:"dir,omitempty"` + Name string `json:"name,omitempty"` + }{ + before, "", + }) + } + } + err = builder.Build(cmd.Root().Context(), output) + if err != nil { + log.Fatalf("[FATAL] %v", err) + } + + // done if we're skipping the build + if builder.SkipBuild { + return nil + } + + // if requested, run setcap to allow binding to low ports + err = setcapIfRequested(output) + if err != nil { + return err + } + + // prove the build is working by printing the version + if runtime.GOOS == utils.GetGOOS() && runtime.GOARCH == utils.GetGOARCH() { + if !filepath.IsAbs(output) { + output = "." + string(filepath.Separator) + output + } + fmt.Println() + fmt.Printf("%s version\n", output) + cmd := exec.Command(output, "version") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + log.Fatalf("[FATAL] %v", err) + } + } + + return nil + }, +} diff --git a/cmd/main.go b/cmd/main.go index c058192..9a898d3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -26,7 +26,6 @@ import ( "os/signal" "path" "path/filepath" - "runtime" "runtime/debug" "strings" @@ -49,160 +48,12 @@ func Main() { defer cancel() go trapSignals(ctx, cancel) - if len(os.Args) > 1 && os.Args[1] == "build" { - if err := runBuild(ctx, os.Args[2:]); err != nil { - log.Fatalf("[ERROR] %v", err) - } - return - } - - if len(os.Args) > 1 && os.Args[1] == "version" { - fmt.Println(xcaddyVersion()) - return - } - - if err := runDev(ctx, os.Args[1:]); err != nil { - log.Fatalf("[ERROR] %v", err) + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) } } -func runBuild(ctx context.Context, args []string) error { - // parse the command line args... rather primitively - var argCaddyVersion, output string - var plugins []xcaddy.Dependency - var replacements []xcaddy.Replace - var embedDir []string - for i := 0; i < len(args); i++ { - switch args[i] { - case "--with", "--replace": - arg := args[i] - if i == len(args)-1 { - return fmt.Errorf("expected value after %s flag", arg) - } - i++ - mod, ver, repl, err := splitWith(args[i]) - if err != nil { - return err - } - mod = strings.TrimSuffix(mod, "/") // easy to accidentally leave a trailing slash if pasting from a URL, but is invalid for Go modules - if arg == "--with" { - plugins = append(plugins, xcaddy.Dependency{ - PackagePath: mod, - Version: ver, - }) - } - - if arg != "--with" && repl == "" { - return fmt.Errorf("expected value after --replace flag") - } - if repl != "" { - // adjust relative replacements in current working directory since our temporary module is in a different directory - if strings.HasPrefix(repl, ".") { - repl, err = filepath.Abs(repl) - if err != nil { - log.Fatalf("[FATAL] %v", err) - } - log.Printf("[INFO] Resolved relative replacement %s to %s", args[i], repl) - } - replacements = append(replacements, xcaddy.NewReplace(xcaddy.Dependency{PackagePath: mod, Version: ver}.String(), repl)) - } - case "--output": - if i == len(args)-1 { - return fmt.Errorf("expected value after --output flag") - } - i++ - output = args[i] - case "--embed": - if i == len(args)-1 { - return fmt.Errorf("expected value after --embed flag") - } - i++ - embedDir = append(embedDir, args[i]) - default: - if argCaddyVersion != "" { - return fmt.Errorf("missing flag; caddy version already set at %s", argCaddyVersion) - } - argCaddyVersion = args[i] - } - } - - // prefer caddy version from command line argument over env var - if argCaddyVersion != "" { - caddyVersion = argCaddyVersion - } - - // ensure an output file is always specified - if output == "" { - output = getCaddyOutputFile() - } - - // perform the build - builder := xcaddy.Builder{ - Compile: xcaddy.Compile{ - Cgo: os.Getenv("CGO_ENABLED") == "1", - }, - CaddyVersion: caddyVersion, - Plugins: plugins, - Replacements: replacements, - RaceDetector: raceDetector, - SkipBuild: skipBuild, - SkipCleanup: skipCleanup, - Debug: buildDebugOutput, - BuildFlags: buildFlags, - ModFlags: modFlags, - } - for _, md := range embedDir { - if before, after, found := strings.Cut(md, ":"); found { - builder.EmbedDirs = append(builder.EmbedDirs, struct { - Dir string `json:"dir,omitempty"` - Name string `json:"name,omitempty"` - }{ - after, before, - }) - } else { - builder.EmbedDirs = append(builder.EmbedDirs, struct { - Dir string `json:"dir,omitempty"` - Name string `json:"name,omitempty"` - }{ - before, "", - }) - } - } - err := builder.Build(ctx, output) - if err != nil { - log.Fatalf("[FATAL] %v", err) - } - - // done if we're skipping the build - if builder.SkipBuild { - return nil - } - - // if requested, run setcap to allow binding to low ports - err = setcapIfRequested(output) - if err != nil { - return err - } - - // prove the build is working by printing the version - if runtime.GOOS == utils.GetGOOS() && runtime.GOARCH == utils.GetGOARCH() { - if !filepath.IsAbs(output) { - output = "." + string(filepath.Separator) + output - } - fmt.Println() - fmt.Printf("%s version\n", output) - cmd := exec.Command(output, "version") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err = cmd.Run() - if err != nil { - log.Fatalf("[FATAL] %v", err) - } - } - - return nil -} - func getCaddyOutputFile() string { f := "." + string(filepath.Separator) + "caddy" // compiling for Windows or compiling on windows without setting GOOS, use .exe extension @@ -212,88 +63,6 @@ func getCaddyOutputFile() string { return f } -func runDev(ctx context.Context, args []string) error { - binOutput := getCaddyOutputFile() - - // get current/main module name and the root directory of the main module - // - // make sure the module being developed is replaced - // so that the local copy is used - // - // replace directives only apply to the top-level/main go.mod, - // and since this tool is a carry-through for the user's actual - // go.mod, we need to transfer their replace directives through - // to the one we're making - cmd := exec.Command(utils.GetGo(), "list", "-mod=readonly", "-m", "-json", "all") - cmd.Stderr = os.Stderr - out, err := cmd.Output() - if err != nil { - return fmt.Errorf("exec %v: %v: %s", cmd.Args, err, string(out)) - } - currentModule, moduleDir, replacements, err := parseGoListJson(out) - if err != nil { - return fmt.Errorf("json parse error: %v", err) - } - - // reconcile remaining path segments; for example if a module foo/a - // is rooted at directory path /home/foo/a, but the current directory - // is /home/foo/a/b, then the package to import should be foo/a/b - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("unable to determine current directory: %v", err) - } - importPath := normalizeImportPath(currentModule, cwd, moduleDir) - - // build caddy with this module plugged in - builder := xcaddy.Builder{ - Compile: xcaddy.Compile{ - Cgo: os.Getenv("CGO_ENABLED") == "1", - }, - CaddyVersion: caddyVersion, - Plugins: []xcaddy.Dependency{ - {PackagePath: importPath}, - }, - Replacements: replacements, - RaceDetector: raceDetector, - SkipBuild: skipBuild, - SkipCleanup: skipCleanup, - Debug: buildDebugOutput, - } - err = builder.Build(ctx, binOutput) - if err != nil { - return err - } - - // if requested, run setcap to allow binding to low ports - err = setcapIfRequested(binOutput) - if err != nil { - return err - } - - log.Printf("[INFO] Running %v\n\n", append([]string{binOutput}, args...)) - - cmd = exec.Command(binOutput, args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err = cmd.Start() - if err != nil { - return err - } - defer func() { - if skipCleanup { - log.Printf("[INFO] Skipping cleanup as requested; leaving artifact: %s", binOutput) - return - } - err = os.Remove(binOutput) - if err != nil && !os.IsNotExist(err) { - log.Printf("[ERROR] Deleting temporary binary %s: %v", binOutput, err) - } - }() - - return cmd.Wait() -} - func setcapIfRequested(output string) error { if os.Getenv("XCADDY_SETCAP") != "1" { return nil diff --git a/go.mod b/go.mod index 9925154..f03b4be 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/josephspurrier/goversioninfo v1.4.0 + github.com/spf13/cobra v1.8.0 ) diff --git a/go.sum b/go.sum index a3d7699..a532f39 100644 --- a/go.sum +++ b/go.sum @@ -2,18 +2,26 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=