Skip to content

Commit

Permalink
Migrated xcaddy to cobra
Browse files Browse the repository at this point in the history
  • Loading branch information
armadi1809 committed Aug 4, 2024
1 parent ff8268a commit 73454d0
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 236 deletions.
119 changes: 119 additions & 0 deletions cmd/cobra.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
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 <args...>",
Long: `If you run xcaddy from within the folder of the Caddy plugin you're working on without the build subcommand, it will build Caddy with your current module and run it, as if you manually plugged it in and invoked go run.
The binary will be built and run from the current directory, then cleaned up.
The current working directory must be inside an initialized Go module.
Syntax:
xcaddy <args...>
* <args...> are passed through to the caddy command.`,
Short: "A replacement for go run while developing Caddy plugins",
SilenceUsage: true,
Version: xcaddyVersion(),
Args: cobra.ArbitraryArgs,
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)
}
190 changes: 190 additions & 0 deletions cmd/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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: "Command to get xcaddy version",
Short: "xcaddy version",
RunE: func(cm *cobra.Command, args []string) error {
fmt.Println(xcaddyVersion())
return nil
},
}

var buildCommand = &cobra.Command{
Use: `build [<caddy_version>]
[--output <file>]
[--with <module[@version][=replacement]>...]
[--replace <module[@version]=replacement>...]
[--embed <[alias]:path/to/dir>...]`,
Long: `
<caddy_version> is the core Caddy version to build; defaults to CADDY_VERSION env variable or latest.
This can be the keyword latest, which will use the latest stable tag, or any git ref such as:
A tag like v2.0.1
A branch like master
A commit like a58f240d3ecbb59285303746406cab50217f8d24
Flags:
--output changes the output file.
--with 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.
--replace 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.
--embed 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.
`,
Short: "Compile custom caddy binaries",
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
},
}
Loading

0 comments on commit 73454d0

Please sign in to comment.