2019-09-23 21:30:20 +03:00

309 lines
8.4 KiB
Go

package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"text/template"
"time"
"github.com/apex/log"
"github.com/apex/log/handlers/cli"
"github.com/client9/codegen/shell"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/goreleaser/goreleaser/pkg/defaults"
"github.com/pkg/errors"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
// nolint: gochecknoglobals
var (
version = "dev"
commit = "none"
datestr = "unknown"
)
// given a template, and a config, generate shell script
func makeShell(tplsrc string, cfg *config.Project) ([]byte, error) {
// if we want to add a timestamp in the templates this
// function will generate it
funcMap := template.FuncMap{
"join": strings.Join,
"platformBinaries": makePlatformBinaries,
"timestamp": func() string {
return time.Now().UTC().Format(time.RFC3339)
},
}
out := bytes.Buffer{}
t, err := template.New("shell").Funcs(funcMap).Parse(tplsrc)
if err != nil {
return nil, err
}
err = t.Execute(&out, cfg)
return out.Bytes(), err
}
// makePlatform returns a platform string combining goos, goarch, and goarm.
func makePlatform(goos, goarch, goarm string) string {
platform := goos + "/" + goarch
if goarch == "arm" && goarm != "" {
platform += "v" + goarm
}
return platform
}
// makePlatformBinaries returns a map from platforms to a slice of binaries
// built for that platform.
func makePlatformBinaries(cfg *config.Project) map[string][]string {
platformBinaries := make(map[string][]string)
for _, build := range cfg.Builds {
ignore := make(map[string]bool)
for _, ignoredBuild := range build.Ignore {
platform := makePlatform(ignoredBuild.Goos, ignoredBuild.Goarch, ignoredBuild.Goarm)
ignore[platform] = true
}
for _, goos := range build.Goos {
for _, goarch := range build.Goarch {
switch goarch {
case "arm":
for _, goarm := range build.Goarm {
platform := makePlatform(goos, goarch, goarm)
if !ignore[platform] {
platformBinaries[platform] = append(platformBinaries[platform], build.Binary)
}
}
default:
platform := makePlatform(goos, goarch, "")
if !ignore[platform] {
platformBinaries[platform] = append(platformBinaries[platform], build.Binary)
}
}
}
}
}
return platformBinaries
}
// converts the given name template to it's equivalent in shell
// except for the default goreleaser templates, templates with
// conditionals will return an error
//
// {{ .Binary }} ---> [prefix]${BINARY}, etc.
//
func makeName(prefix, target string) (string, error) {
// armv6 is the default in the shell script
// so do not need special template condition for ARM
armversion := "{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
target = strings.Replace(target, armversion, "{{ .Arch }}", -1)
// hack for https://github.com/goreleaser/godownloader/issues/70
armversion = "{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}"
target = strings.Replace(target, armversion, "{{ .Arch }}", -1)
// otherwise if it contains a conditional, we can't (easily)
// translate that to bash. Ask for bug report.
if strings.Contains(target, "{{ if") ||
strings.Contains(target, "{{if") ||
strings.Contains(target, "{{ .Arm") ||
strings.Contains(target, "{{.Arm") {
//nolint: lll
return "", fmt.Errorf("name_template %q contains unknown conditional or ARM format. Please file bug at https://github.com/goreleaser/godownloader", target)
}
varmap := map[string]string{
"Os": "${OS}",
"Arch": "${ARCH}",
"Version": "${VERSION}",
"Tag": "${TAG}",
"Binary": "${BINARY}",
"ProjectName": "${PROJECT_NAME}",
}
out := bytes.Buffer{}
if _, err := out.WriteString(prefix); err != nil {
return "", err
}
t, err := template.New("name").Parse(target)
if err != nil {
return "", err
}
err = t.Execute(&out, varmap)
return out.String(), err
}
// returns the owner/name repo from input
//
// see https://github.com/goreleaser/godownloader/issues/55
func normalizeRepo(repo string) string {
// handle full or partial URLs
repo = strings.TrimPrefix(repo, "https://github.com/")
repo = strings.TrimPrefix(repo, "http://github.com/")
repo = strings.TrimPrefix(repo, "github.com/")
// hande /name/repo or name/repo/ cases
repo = strings.Trim(repo, "/")
return repo
}
func loadURLs(path, configPath string) (*config.Project, error) {
for _, file := range []string{configPath, "goreleaser.yml", ".goreleaser.yml", "goreleaser.yaml", ".goreleaser.yaml"} {
if file == "" {
continue
}
url := fmt.Sprintf("%s/%s", path, file)
log.Infof("reading %s", url)
project, err := loadURL(url)
if err != nil {
return nil, err
}
if project != nil {
return project, nil
}
}
return nil, fmt.Errorf("could not fetch a goreleaser configuration file")
}
func loadURL(file string) (*config.Project, error) {
// nolint: gosec
resp, err := http.Get(file)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
log.Errorf("reading %s returned %d %s\n", file, resp.StatusCode, http.StatusText(resp.StatusCode))
return nil, nil
}
p, err := config.LoadReader(resp.Body)
// to make errcheck happy
errc := resp.Body.Close()
if errc != nil {
return nil, errc
}
return &p, err
}
func loadFile(file string) (*config.Project, error) {
p, err := config.Load(file)
return &p, err
}
// Load project configuration from a given repo name or filepath/url.
func Load(repo, configPath, file string) (project *config.Project, err error) {
if repo == "" && file == "" {
return nil, fmt.Errorf("repo or file not specified")
}
if file == "" {
repo = normalizeRepo(repo)
log.Infof("reading repo %q on github", repo)
project, err = loadURLs(
fmt.Sprintf("https://raw.githubusercontent.com/%s/master", repo),
configPath,
)
} else {
log.Infof("reading file %q", file)
project, err = loadFile(file)
}
if err != nil {
return nil, err
}
// if not specified add in GitHub owner/repo info
if project.Release.GitHub.Owner == "" {
if repo == "" {
return nil, fmt.Errorf("owner/name repo not specified")
}
project.Release.GitHub.Owner = path.Dir(repo)
project.Release.GitHub.Name = path.Base(repo)
}
var ctx = context.New(*project)
for _, defaulter := range defaults.Defaulters {
log.Infof("setting defaults for %s", defaulter)
if err := defaulter.Default(ctx); err != nil {
return nil, errors.Wrap(err, "failed to set defaults")
}
}
project = &ctx.Config
// set default binary name
if len(project.Builds) == 0 {
project.Builds = []config.Build{
{Binary: path.Base(repo)},
}
}
if project.Builds[0].Binary == "" {
project.Builds[0].Binary = path.Base(repo)
}
return project, err
}
func main() {
log.SetHandler(cli.Default)
var (
repo = kingpin.Flag("repo", "owner/name or URL of GitHub repository").Short('r').String()
output = kingpin.Flag("output", "output file, default stdout").Short('o').String()
force = kingpin.Flag("force", "force writing of output").Short('f').Bool()
source = kingpin.Flag("source", "source type [godownloader|raw|equinoxio]").Default("godownloader").String()
exe = kingpin.Flag("exe", "name of binary, used only in raw").String()
nametpl = kingpin.Flag("nametpl", "name template, used only in raw").String()
tree = kingpin.Flag("tree", "use tree to generate multiple outputs").String()
file = kingpin.Arg("file", "??").String()
)
kingpin.CommandLine.Version(fmt.Sprintf("%v, commit %v, built at %v", version, commit, datestr))
kingpin.CommandLine.VersionFlag.Short('v')
kingpin.CommandLine.HelpFlag.Short('h')
kingpin.Parse()
if *tree != "" {
err := treewalk(*tree, *file, *force)
if err != nil {
log.WithError(err).Error("treewalker failed")
os.Exit(1)
}
return
}
// gross.. need config
out, err := processSource(*source, *repo, "", *file, *exe, *nametpl)
if err != nil {
log.WithError(err).Error("failed")
os.Exit(1)
}
// stdout case
if *output == "" {
if _, err = os.Stdout.Write(out); err != nil {
log.WithError(err).Error("unable to write")
os.Exit(1)
}
return
}
// only write out if forced to, OR if output is effectively different
// than what the file has.
if *force || shell.ShouldWriteFile(*output, out) {
if err = ioutil.WriteFile(*output, out, 0666); err != nil {
log.WithError(err).Errorf("unable to write to %s", *output)
os.Exit(1)
}
return
}
// output is effectively the same as new content
// (comments and most whitespace doesn't matter)
// nothing to do
}