Update CLI; change command to 'xcaddy'; update readme

This commit is contained in:
Matthew Holt 2020-03-25 00:01:44 -06:00
parent 40c0796cc2
commit 1cc8e08c16
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
5 changed files with 234 additions and 105 deletions

View file

@ -5,6 +5,8 @@ This package and associated CLI tool make it easy to perform custom builds of th
Supports Caddy 2 and up.
⚠️ Still in early development. Works; but no guarantees and prone to changes at this point. Stay updated and please submit feedback!
## Requirements
- Go installed
@ -14,9 +16,9 @@ Supports Caddy 2 and up.
## Library usage
```go
caddyVersion := "v2.0.0-beta.19"
plugins := []builder.CaddyPlugin{
builder.CaddyPlugin{
caddyVersion := "v2.0.0-beta.20"
plugins := []builder.Dependency{
builder.Dependency{
ModulePath: "github.com/caddyserver/nginx-adapter",
Version: "6c484552e630ccac384d2d9c43c9d14c4e8d2e56",
},
@ -31,23 +33,52 @@ Versions can be anything compatible with `go get`.
## CLI usage
The CLI can be used both to make custom builds of Caddy, but also as a replacement for `go run` while developing Caddy plugins.
### For custom builds
Syntax:
```
builder --version <version> [--output <file>] [<plugins...>]
xcaddy build <version>
[--output <file>]
[--with <module[@version]>...]
```
Where:
- `--version` is the core Caddy version to build (required).
- `--version` is the core Caddy version to build (required, for now).
- `--output` changes the output file.
- `<plugins...>` are extra plugins and their versions, in `go get` module syntax: `module@version`
- `--with` can be used multiple times to add plugins by specifying the module name and optionally its version, in a way similar to `go get`.
For example:
```bash
$ builder --version v2.0.0-beta.19 \
github.com/caddyserver/nginx-adapter@6c484552e630ccac384d2d9c43c9d14c4e8d2e56
$ xcaddy build v2.0.0-beta.20 \
--with github.com/caddyserver/nginx-adapter@6c484552e630ccac384d2d9c43c9d14c4e8d2e56
```
### For plugin development
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, similar to if you manually plugged it in and ran `go run`.
The binary will be built, run from the current directory, then cleaned up.
Syntax:
```
xcaddy <args...>
```
Where:
- `<args...>` are passed through to the `caddy` command.
For example:
```bash
$ xcaddy list-modules
$ xcaddy run --config caddy.json
```

View file

@ -18,6 +18,7 @@ import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"regexp"
@ -29,9 +30,9 @@ import (
"github.com/Masterminds/semver/v3"
)
// Build builds Caddy at the given version with the given plugins and plops
// binary down at outputFile.
func Build(caddyVersion string, plugins []CaddyPlugin, outputFile string) error {
// Build builds Caddy at the given version with the given plugins and
// plops a binary down at outputFile.
func Build(caddyVersion string, deps []Dependency, outputFile string) error {
if caddyVersion == "" {
return fmt.Errorf("caddy version is required")
}
@ -47,7 +48,7 @@ func Build(caddyVersion string, plugins []CaddyPlugin, outputFile string) error
return err
}
env, err := newEnvironment(caddyVersion, plugins)
env, err := newEnvironment(caddyVersion, deps)
if err != nil {
return err
}
@ -60,6 +61,7 @@ func Build(caddyVersion string, plugins []CaddyPlugin, outputFile string) error
"-ldflags", "-w -s", // trim debug symbols
"-trimpath",
)
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
err = env.runCommand(cmd, 5*time.Minute)
if err != nil {
return err
@ -70,10 +72,11 @@ func Build(caddyVersion string, plugins []CaddyPlugin, outputFile string) error
return nil
}
// CaddyPlugin pairs a Go module path with a version.
type CaddyPlugin struct {
// Dependency pairs a Go module path with a version.
type Dependency struct {
ModulePath string
Version string
Replace string
}
// newTempFolder creates a new folder in a temporary location.

View file

@ -1,89 +0,0 @@
// Copyright 2020 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/caddyserver/builder"
)
func init() {
flag.StringVar(&caddyVersion, "version", "", "The version of Caddy core")
flag.StringVar(&output, "output", "", "Where to save the resulting binary")
}
func main() {
flag.Parse()
if caddyVersion == "" {
log.Fatal("[FATAL] Caddy version is required (use --version)")
}
// assemble list of plugins and their versions
var plugins []builder.CaddyPlugin
for _, arg := range flag.Args() {
parts := strings.SplitN(arg, "@", 2)
if len(parts) != 2 {
log.Fatalf("[FATAL] %s: Plugins must be defined using 'module@version' format, similar to the `go get` command", arg)
}
plugins = append(plugins, builder.CaddyPlugin{
ModulePath: parts[0],
Version: parts[1],
})
}
// ensure an output file is always specified
if output == "" {
if runtime.GOOS == "windows" {
output = "caddy.exe"
} else {
output = "caddy"
}
}
// perform the build
err := builder.Build(caddyVersion, plugins, output)
if err != nil {
log.Fatalf("[FATAL] %v", err)
}
// prove the build is working by printing the version
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)
}
}
var (
caddyVersion string
plugins []string
output string
)

168
cmd/xcaddy/main.go Normal file
View file

@ -0,0 +1,168 @@
// Copyright 2020 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/caddyserver/builder"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "build" {
if err := runBuild(os.Args[2:]); err != nil {
log.Fatalf("[ERROR] %v", err)
}
return
}
// TODO: the caddy version needs to be settable by the user... maybe an env var?
if err := runDev("v2.0.0-beta.20", os.Args[1:]); err != nil {
log.Fatalf("[ERROR] %v", err)
}
}
func runBuild(args []string) error {
// parse the command line args... rather primitively
var caddyVersion, output string
var plugins []builder.Dependency
for i := 0; i < len(args); i++ {
switch args[i] {
case "--with":
if i == len(args)-1 {
return fmt.Errorf("expected value after --with flag")
}
i++
var mod, ver string
arg := args[i]
parts := strings.SplitN(arg, "@", 2)
mod = parts[0]
if len(parts) == 2 {
ver = parts[1]
}
plugins = append(plugins, builder.Dependency{
ModulePath: mod,
Version: ver,
})
case "--output":
if i == len(args)-1 {
return fmt.Errorf("expected value after --output flag")
}
i++
output = args[i+1]
default:
if caddyVersion != "" {
return fmt.Errorf("missing flag; caddy version already set at %s", caddyVersion)
}
caddyVersion = args[i]
}
}
// ensure an output file is always specified
if output == "" {
if runtime.GOOS == "windows" {
output = "caddy.exe"
} else {
output = "caddy"
}
}
// perform the build
err := builder.Build(caddyVersion, plugins, output)
if err != nil {
log.Fatalf("[FATAL] %v", err)
}
// prove the build is working by printing the version
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 runDev(caddyVersion string, args []string) error {
const binOutput = "./caddy"
// get current/main module name
cmd := exec.Command("go", "list", "-m")
out, err := cmd.Output()
if err != nil {
return err
}
currentModule := strings.TrimSpace(string(out))
// get its root directory
cmd = exec.Command("go", "list", "-m", "-f={{.Dir}}")
out, err = cmd.Output()
if err != nil {
return err
}
moduleDir := strings.TrimSpace(string(out))
// build caddy with this module plugged in
err = builder.Build(caddyVersion, []builder.Dependency{
{
ModulePath: currentModule,
Replace: moduleDir,
},
}, 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
}
cleanup := func() {
err = os.Remove(binOutput)
if err != nil && !os.IsNotExist(err) {
log.Printf("[ERROR] Deleting temporary binary %s: %v", binOutput, err)
}
}
defer cleanup()
go func() {
time.Sleep(2 * time.Second)
cleanup()
}()
return cmd.Wait()
}

View file

@ -27,7 +27,7 @@ import (
"time"
)
func newEnvironment(caddyVersion string, plugins []CaddyPlugin) (*environment, error) {
func newEnvironment(caddyVersion string, plugins []Dependency) (*environment, error) {
// assume v2 if no semantic version is provided
caddyModulePath := defaultCaddyModulePath
if !strings.HasPrefix(caddyVersion, "v") || !strings.Contains(caddyVersion, ".") {
@ -103,6 +103,19 @@ func newEnvironment(caddyVersion string, plugins []CaddyPlugin) (*environment, e
return nil, err
}
// specify module replacements before pinning versions
for _, p := range plugins {
if p.Replace != "" {
log.Printf("[INFO] Replace %s => %s", p.ModulePath, p.Replace)
cmd := env.newCommand("go", "mod", "edit",
"-replace", fmt.Sprintf("%s=%s", p.ModulePath, p.Replace))
err := env.runCommand(cmd, 10*time.Second)
if err != nil {
return nil, err
}
}
}
// pin versions by populating go.mod, first for Caddy itself and then plugins
log.Println("[INFO] Pinning versions")
err = env.execGoGet(caddyModulePath, caddyVersion)
@ -110,6 +123,9 @@ func newEnvironment(caddyVersion string, plugins []CaddyPlugin) (*environment, e
return nil, err
}
for _, p := range plugins {
if p.Replace != "" {
continue
}
err = env.execGoGet(p.ModulePath, p.Version)
if err != nil {
return nil, err
@ -123,7 +139,7 @@ func newEnvironment(caddyVersion string, plugins []CaddyPlugin) (*environment, e
type environment struct {
caddyVersion string
plugins []CaddyPlugin
plugins []Dependency
caddyModulePath string
tempFolder string
}