309 lines
8.4 KiB
Go
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
|
|
}
|