package main
import (
"strings"
"github.com/mholt/archiver"
"github.com/pkg/errors"
)
func archive(archivePath string, archiveType string, files []string) error {
switch strings.ToLower(archiveType) {
case "zip":
zip := archiver.NewZip()
if err := zip.Archive(files, archivePath); err != nil {
return errors.Wrap(err, "archving zip")
}
case "tar":
tar := archiver.NewTar()
if err := tar.Archive(files, archivePath); err != nil {
return errors.Wrap(err, "archving tar")
}
case "tbz2", "tar.bz2":
tarbz2 := archiver.NewTarBz2()
if err := tarbz2.Archive(files, archivePath); err != nil {
return errors.Wrap(err, "archving tar.bz2")
}
case "tgz", "tar.gz":
targz := archiver.NewTarGz()
if err := targz.Archive(files, archivePath); err != nil {
return errors.Wrap(err, "archving tar.gz")
}
case "tlz4", "tar.lz4":
tarlz4 := archiver.NewTarLz4()
if err := tarlz4.Archive(files, archivePath); err != nil {
return errors.Wrap(err, "archving tar.lz4")
}
case "tsz", "tar.sz":
tarsz := archiver.NewTarSz()
if err := tarsz.Archive(files, archivePath); err != nil {
return errors.Wrap(err, "archving tar.sz")
}
case "txz", "tar.xz":
tarxz := archiver.NewTarXz()
if err := tarxz.Archive(files, archivePath); err != nil {
return errors.Wrap(err, "archving tar.xz")
}
default:
return errors.Errorf("unknown archving format '%s'", archiveType)
}
return nil
}
package main
import (
"bytes"
"fmt"
"html/template"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/gesquive/cli"
"github.com/pkg/errors"
)
// Package is a combination of OS/arch/archive that can be packaged.
type Package struct {
OS string
Arch string
Archive string
ExePath string
ArchivePath string
FileList []string
Dir string
}
func (p *Package) String() string {
return fmt.Sprintf("%s/%s/%s", p.OS, p.Arch, p.Archive)
}
func ParsePackage(pkgString string) (Package, error) {
pkg := Package{}
parts := strings.SplitN(pkgString, "/", 3)
if len(parts) != 3 {
return pkg, errors.Errorf("could not parse package '%s'", pkgString)
}
pkg.OS = parts[0]
pkg.Arch = parts[1]
pkg.Archive = parts[2]
return pkg, nil
}
var (
// OSList is the full list of golang OSs
OSList = []string{
"darwin",
"dragonfly",
"freebsd",
"linux",
"netbsd",
"openbsd",
"plan9",
"solaris",
"windows",
}
// ArchList is the full list of golang architectures
ArchList = []string{
"386",
"amd64",
"amd64p32",
"arm",
"arm64",
"ppc64",
"ppc64le",
}
// DefaultArchiveList is the list of default archives
DefaultArchiveList = []string{
"zip",
"tar.gz",
"tar.xz",
}
// ArchiveList is the full list of supported archives
ArchiveList = []string{
"zip",
"tar",
"tar.gz",
"tar.bz2",
"tar.xz",
"tar.lz4",
"tar.sz",
}
)
// GetUserArchs generates a list of architectures from the user defined list
func GetUserArchs(userArch []string) ([]string, error) {
cleanList := splitListItems(userArch)
pList, nList := splitNegatedItems(cleanList)
if len(pList) == 0 {
pList = ArchList
}
cleanList = negateList(pList, nList)
return cleanList, nil
}
// GetUserOSs generates a list of OSs from the user defined list
func GetUserOSs(userOS []string) ([]string, error) {
cleanList := splitListItems(userOS)
pList, nList := splitNegatedItems(cleanList)
if len(pList) == 0 {
pList = OSList
}
cleanList = negateList(pList, nList)
return cleanList, nil
}
// GetUserArchives generates a list of valid archive types from the user defined list
func GetUserArchives(userArchive []string) ([]string, error) {
cleanList := splitListItems(userArchive)
pList, nList := splitNegatedItems(cleanList)
if len(pList) == 0 {
pList = ArchiveList
}
cleanList = negateList(pList, nList)
validArchives := []string{}
for _, archive := range cleanList {
for _, dArchive := range ArchiveList {
if strings.ToLower(archive) == dArchive {
validArchives = append(validArchives, archive)
break
}
}
}
return validArchives, nil
}
func GetUserPackages(userPkgs []string) ([]Package, error) {
pkgs := []Package{}
userPkgs = splitListItems(userPkgs)
for _, userPkg := range userPkgs {
pkg, err := ParsePackage(userPkg)
if err != nil {
continue
}
pkgs = append(pkgs, pkg)
}
return pkgs, nil
}
// AssemblePackageInfo generates a list of packages from the user defined arguments
func AssemblePackageInfo(userArch []string, userOS []string,
userArchive []string, userPackages []string) ([]Package, error) {
archList, _ := GetUserArchs(userArch)
osList, _ := GetUserOSs(userOS)
archiveList, _ := GetUserArchives(userArchive)
specificList, _ := GetUserPackages(userPackages)
packageList := []Package{}
for _, arch := range archList {
for _, os := range osList {
for _, archive := range archiveList {
pkg := Package{Arch: arch, OS: os, Archive: archive}
packageList = appendIfMissing(packageList, pkg)
}
}
}
for _, userPkg := range specificList {
if strings.HasPrefix(userPkg.String(), "!") {
userPkg.OS = userPkg.OS[1:]
packageList = removeIfPresent(packageList, userPkg)
} else {
packageList = appendIfMissing(packageList, userPkg)
}
}
return packageList, nil
}
// GetPackagePaths generates info about the archives
func GetPackagePaths(packages []Package, dirs []string, inputTemplate string,
outputTemplate string) ([]Package, error) {
filledPackages := []Package{}
for _, pkg := range packages {
for _, path := range dirs {
filledPkg := Package{
Dir: filepath.Base(path),
OS: pkg.OS,
Arch: pkg.Arch,
Archive: pkg.Archive,
}
inputTpl, err := template.New("input").Parse(inputTemplate)
if err != nil {
return nil, errors.Wrap(err, "input template error")
}
var inputPath bytes.Buffer
if err = inputTpl.Execute(&inputPath, &filledPkg); err != nil {
return nil, errors.Wrap(err, "error generating input path")
}
filledPkg.ExePath = inputPath.String()
if strings.ToLower(filledPkg.OS) == "windows" {
filledPkg.ExePath = fmt.Sprintf("%s.exe", filledPkg.ExePath)
}
outputTpl, err := template.New("output").Parse(outputTemplate)
if err != nil {
return nil, errors.Wrap(err, "output template error")
}
var outputPath bytes.Buffer
if err := outputTpl.Execute(&outputPath, &filledPkg); err != nil {
return nil, errors.Wrap(err, "error generating output path")
}
filledPkg.ArchivePath = outputPath.String()
filledPackages = append(filledPackages, filledPkg)
}
}
return filledPackages, nil
}
func GetPackageFiles(packages []Package, fileList []string) ([]Package, error) {
pkgs := []Package{}
fileList = splitListItems(fileList)
for _, pkg := range packages {
files := append([]string{pkg.ExePath}, fileList...)
pkg.FileList = files
pkgs = append(pkgs, pkg)
}
return pkgs, nil
}
// GetAppDirs returns the file paths to the packages that are "main"
// packages, from the list of packages given. The list of packages can
// include relative paths, the special "..." Go keyword, etc.
func GetAppDirs(packages []string) ([]string, error) {
if len(packages) < 1 {
packages = []string{"."}
}
// Get the packages that are in the given paths
args := make([]string, 0, len(packages)+3)
args = append(args, "list", "-f", "{{.Name}}|{{.ImportPath}}")
args = append(args, packages...)
output, err := execGo("go", nil, "", args...)
if err != nil {
return nil, err
}
results := make([]string, 0, len(output))
for _, line := range strings.Split(output, "\n") {
if line == "" {
continue
}
parts := strings.SplitN(line, "|", 2)
if len(parts) != 2 {
cli.Warn("Bad line reading packages: %s", line)
continue
}
if parts[0] == "main" {
results = append(results, parts[1])
}
}
return results, nil
}
func splitListItems(list []string) []string {
cleanList := []string{}
for _, item := range list {
if parts := strings.Split(item, " "); len(parts) > 1 {
cleanList = append(cleanList, parts...)
} else if parts := strings.Split(item, ","); len(parts) > 1 {
cleanList = append(cleanList, parts...)
} else {
cleanList = append(cleanList, item)
}
}
return cleanList
}
func splitNegatedItems(list []string) (p []string, n []string) {
for _, item := range list {
if strings.HasPrefix(item, "!") {
n = append(n, item[1:])
} else {
p = append(p, item)
}
}
return
}
func negateList(pList []string, nList []string) []string {
finalList := []string{}
for _, item := range pList {
lowerItem := strings.ToLower(item)
found := false
for _, negation := range nList {
if lowerItem == strings.ToLower(negation) {
found = true
break
}
}
if !found {
finalList = append(finalList, item)
}
}
return finalList
}
func removeIfPresent(pkgs []Package, pkg Package) []Package {
match := strings.ToLower(pkg.String())
result := []Package{}
for _, existing := range pkgs {
if strings.ToLower(existing.String()) != match {
result = append(result, existing)
}
}
return result
}
func appendIfMissing(pkgs []Package, pkg Package) []Package {
match := strings.ToLower(pkg.String())
missing := true
for _, existing := range pkgs {
if strings.ToLower(existing.String()) == match {
missing = false
}
}
if missing {
pkgs = append(pkgs, pkg)
}
return pkgs
}
// NOTE: The original code can be found at the gox repo
// https://raw.githubusercontent.com/mitchellh/gox/master/go.go
func execGo(GoCmd string, env []string, dir string, args ...string) (string, error) {
var stderr, stdout bytes.Buffer
cmd := exec.Command(GoCmd, args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if env != nil {
cmd.Env = env
}
if dir != "" {
cmd.Dir = dir
}
if err := cmd.Run(); err != nil {
err = fmt.Errorf("%s\nStderr: %s", err, stderr.String())
return "", err
}
return stdout.String(), nil
}
// IsEmpty checks to see if a directory is empty
// src: https://stackoverflow.com/a/30708914/613218
func IsEmpty(name string) (bool, error) {
f, err := os.Open(name)
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdirnames(1) // Or f.Readdir(1)
if err == io.EOF {
return true, nil
}
return false, err // Either not empty or error, suits both cases
}
package main
import (
"fmt"
"os"
"path"
"runtime"
"github.com/gesquive/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var debug bool
var showVersion bool
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "gop [flags] [packages]",
Short: "Package your executables",
Long: `Package your multi-os/arch executables
If no specific operating systems, architectures or archives are specified, gop
will search for all known builds and package any found.
Input/Output path template:
The input & output path for the binaries/packages is specified with the
"--input" and "--output" flags respectively. The value is a string that
is a Go text template. The default values are "{{.Dir}}_{{.OS}}_{{.Arch}}"
and "{{.Dir}}_{{.OS}}_{{.Arch}}.{{.Archive}}". The variables and
their values should be self-explanatory.
Packages (OS/Arch/Archive):
The operating systems, architectures, and archives to package may be
specified with the "--arch", "--os" & "--archive" flags. These are space
separated lists of values to build for, respectively. You may prefix an
OS, Arch or Archive with "!" to negate and not package for that value.
If the list is made up of only negations, then the negations will come from
the default list.
Additionally, the "--packages" flag may be used to specify complete
os/arch/archive values that should be built or ignored. The syntax for
this is what you would expect: "linux/amd64/zip" would be a valid package
value. Multiple values can be space separated. An os/arch/archive definition
can begin with "!" to not build for that platform.
The "--packages" flag has the highest precedent when determing whether to
build for a platform. If it is included in the "--packages" list, it will be
built even if the specific os, arch or archive is negated in the "--os",
"--arch" and "--archive" flags respectively.
`,
PersistentPreRun: preRun,
Run: run,
}
// Execute adds all child commands to the root command sets flags appropriately.
func Execute() {
RootCmd.SetHelpTemplate(fmt.Sprintf("%s\nVersion:\n github.com/gesquive/gop %s\n",
RootCmd.HelpTemplate(), buildVersion))
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
}
}
func init() {
cobra.OnInitialize(initConfig)
RootCmd.PersistentFlags().StringP("config", "c", "",
"config file (default .gop.yml)")
RootCmd.PersistentFlags().BoolVarP(&debug, "debug", "D", false,
"Write debug messages to console")
RootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "V", false,
"Show the version and exit")
RootCmd.PersistentFlags().StringP("input", "i", "{{.Dir}}_{{.OS}}_{{.Arch}}",
"The input path template.")
RootCmd.PersistentFlags().StringP("output", "o", "{{.Dir}}_{{.OS}}_{{.Arch}}.{{.Archive}}",
"The output path template.")
RootCmd.PersistentFlags().StringSliceP("files", "f", []string{},
"Add additional file to package")
RootCmd.PersistentFlags().StringSliceP("archive", "r", DefaultArchiveList,
"List of package types to create")
RootCmd.PersistentFlags().StringSliceP("os", "s", OSList,
"List of operating systems to package")
RootCmd.PersistentFlags().StringSliceP("arch", "a", ArchList,
"List of architectures to package")
RootCmd.PersistentFlags().StringSliceP("packages", "p", []string{},
"List of os/arch/archive groups to package")
RootCmd.PersistentFlags().BoolP("delete", "d", false,
"Delete the packaged executables")
RootCmd.PersistentFlags().MarkHidden("debug")
viper.SetEnvPrefix("gop")
viper.AutomaticEnv()
viper.BindEnv("config")
viper.BindEnv("input")
viper.BindEnv("output")
viper.BindEnv("files")
viper.BindEnv("archive")
viper.BindEnv("os")
viper.BindEnv("arch")
viper.BindEnv("packages")
viper.BindEnv("delete")
viper.BindPFlag("config", RootCmd.PersistentFlags().Lookup("config"))
viper.BindPFlag("input", RootCmd.PersistentFlags().Lookup("input"))
viper.BindPFlag("output", RootCmd.PersistentFlags().Lookup("output"))
viper.BindPFlag("files", RootCmd.PersistentFlags().Lookup("files"))
viper.BindPFlag("archive", RootCmd.PersistentFlags().Lookup("archive"))
viper.BindPFlag("os", RootCmd.PersistentFlags().Lookup("os"))
viper.BindPFlag("arch", RootCmd.PersistentFlags().Lookup("arch"))
viper.BindPFlag("packages", RootCmd.PersistentFlags().Lookup("packages"))
viper.BindPFlag("delete", RootCmd.PersistentFlags().Lookup("delete"))
viper.SetDefault("input", "{{.Dir}}_{{.OS}}_{{.Arch}}")
viper.SetDefault("output", "{{.Dir}}_{{.OS}}_{{.Arch}}.{{.Archive}}")
viper.SetDefault("archive", DefaultArchiveList)
viper.SetDefault("os", OSList)
viper.SetDefault("arch", ArchList)
viper.SetDefault("delete", false)
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
cfgFile := viper.GetString("config")
if cfgFile != "" { // enable ability to specify config file via flag
viper.SetConfigFile(cfgFile)
} else {
viper.SetConfigName(".gop") // name of config file (without extension)
viper.AddConfigPath(".") // adding current directory as first search path
viper.AddConfigPath("$HOME/.config/.gop") // adding home directory as next search path
}
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
return
}
if !showVersion {
fmt.Println("Error opening config: ", err)
}
}
}
func preRun(cmd *cobra.Command, args []string) {
if showVersion {
fmt.Printf("github.com/gesquive/gop\n")
fmt.Printf(" Version: %s\n", buildVersion)
if len(buildCommit) > 6 {
fmt.Printf(" Git Commit: %s\n", buildCommit[:7])
}
if buildDate != "" {
fmt.Printf(" Build Date: %s\n", buildDate)
}
fmt.Printf(" Go Version: %s\n", runtime.Version())
fmt.Printf(" OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
if debug {
cli.SetPrintLevel(cli.LevelDebug)
}
cli.Debug("Running with debug turned on")
cli.Debug("config: %s", viper.ConfigFileUsed())
}
func run(cmd *cobra.Command, args []string) {
srcPackages := args
if len(srcPackages) < 1 {
srcPackages = []string{"."}
}
cli.Debug("cfg: packages=%v", srcPackages)
inputTemplate := viper.GetString("input")
cli.Debug("cfg: input=%s", inputTemplate)
outputTemplate := viper.GetString("output")
cli.Debug("cfg: output=%s", outputTemplate)
fileList := viper.GetStringSlice("files")
cli.Debug("cfg: files=%v", fileList)
archList := viper.GetStringSlice("arch")
cli.Debug("cfg: arch=%v", archList)
osList := viper.GetStringSlice("os")
cli.Debug("cfg: os=%v", osList)
archiveList := viper.GetStringSlice("archive")
cli.Debug("cfg: archive=%v", archiveList)
// Get the packages that are in the given paths
appDirs, err := GetAppDirs(srcPackages)
if err != nil {
cli.Fatal("error getting app dirs: %s", err)
}
userPackages := viper.GetStringSlice("packages")
packages, err := AssemblePackageInfo(archList, osList, archiveList, userPackages)
if err != nil {
cli.Fatal("error getting package list: %s", err)
}
cli.Debug("packages found: %s", packages)
packages, err = GetPackagePaths(packages, appDirs, inputTemplate, outputTemplate)
if err != nil {
cli.Fatal("error getting package paths: %s", err)
}
packages, err = GetPackageFiles(packages, fileList)
if err != nil {
cli.Fatal("error getting package files: %s", err)
}
cli.Info("Packaging archives:")
for _, pkg := range packages {
if _, err := os.Stat(pkg.ExePath); os.IsNotExist(err) {
cli.Debug("xxx %60s", pkg.ArchivePath)
continue
}
cli.Info("--> %60s", pkg.ArchivePath)
err = archive(pkg.ArchivePath, pkg.Archive, pkg.FileList)
if err != nil {
cli.Error("error: %s", err)
}
}
cli.Debug("cfg: delete=%t", viper.GetBool("delete"))
if viper.GetBool("delete") {
cli.Info("Cleaning up executables")
for _, pkg := range packages {
os.Remove(pkg.ExePath)
dir := path.Dir(pkg.ExePath)
if isEmpty, _ := IsEmpty(dir); isEmpty {
os.Remove(dir)
}
}
}
}
package main
var (
buildVersion = "v0.2.5-dev"
buildCommit = ""
buildDate = ""
)
func main() {
Execute()
}