Mage - nástroj pro sestavování (nejen) Go programů

Publikováno 12-13-2018 11:17:07

Sastavování (kompilace + linkování) v Go je jedna z věcí, kde vládne organizovaný chaos. Samozřejmě go build prostě funguje. Ale v dnešní době je potřeba dělat mnohem víc věcí, než prosté go build. Bohužel nějaký standardní způsob neexistuje. Jelikož jsem odkojený Un*xem, tak preferuji nástroje, které se samy nabízejí - make. Člověk si udělá makefile a je spokojený - jako například tento. Nevýhodou makefilů je, že ne všichni jim rozumí, a i ti, kdo jim rozumí, obecně neznají všechny finty. Ruku na srdce, kdo ví, co dělá na příklad toto go fmt $$(go list ./... | grep -v /vendor/).

Několik chytrých hlav si tedy sedlo a řeklo, že by bylo potřeba si udělat něco, co by více vyhovovalo porgramátorům v #Go. A tak vznikl mage. Je to nástroj na sestavování programů, který je ovšem vlastně #Go + doplňující knihovny. Spustí se jednoduše pomocí mage a když jde vše dobře, tak na konci je sestavený program. Nebo něco jiného, co chcete dělat.

Nejdřív si tedy mage nainstalujeme.

go get -u -d github.com/magefile/mage\
cd $GOPATH/src/github.com/magefile/mage\
go run bootstrap.go

Zkuste si teď na konzoli zadat mage -version a měli byste vidět smysluplný výstup.

Nyní se ponořte do projektu, pro který byste rádi vytvořili magefile a zadejte mage -init. Po chvilce byste měli vidět v tomto adresáři magefile.go. Zajdet mage -l a měli byte vidět něco jako

T a r b c i i g u l n n e i e s s t l a t t s d n a a : l l l l D e p s A u A M p a b c n u a u a i f s g l t t e d e o r m y s o t y i u e o n r p u s r t d t s a e h e l p a l l s t f , s r t o e e r q p u r i i u r f n e n s y i o n a u g d d n p i e a t e c i d k o a n y g a o e l u r m p a a b n r i a a n g m e s s r , o s m . o e r p l p a l c a e t f o o t r h m e r s p t e h c a i n f i g c o / s b t i e n p s f o r e x a m p l e

Mage operuje s tím, co známe v make jako cíle - targets. Stejně jako v make i zde mohou cíle záviset na jiných cílech. Samotný cíl je vlastně POGoF - plain old #Go function. Tedy až na to, že musí mít signaturu func Target() error, func Target(), func Target(context.Context) error, func Target(context.Context). Samotný magefile musí mít build tag // +build mage.

Pokud chceme, aby cíl závisel na jiném cíli, stačí jen přidat mg.Deps(Target) kde target je funkce jiného cíle. Samozřejmě je potřeba mít naimportovaný import "github.com/magefile/mage/mg".

Pokud už tedy znáte základy mage není problém si ukázat, jak se v mage dělají běžné úkony.

Kompilace Go

Toto je podle celkem realistický target Build. Tedy dobrý startovací blok, který nejen sestaví appku, ale dá jí i trochu fazónu. Jen pozor

import "github.com/magefile/mage/sh"
import "time"
import "unicode"
import "golang.org/x/text/transform"
import "golang.org/x/text/unicode/norm"


var ldflags = "-s -w -X main.AUTHOR=${AUTHOR} -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BRANCH=${BRANCH} -X main.TAG=${TAG} -X main.BUILDTIME=${BUILDTIME}"
var goexe="go"
func flagEnv() map[string]string {
	hash, _ := sh.Output("git", "rev-parse", "--short", "HEAD")
	branch, _ := sh.Output("git", "rev-parse", "--abbrev-ref", "HEAD")
	author, _ := sh.Output("git", "log", "-1", "--pretty=format:'%an'")
	version, _ := sh.Output("git", "describe", "--tags", "--abbrev=0")
   Goarch := "amd64"
   Goos := "linux"
	return map[string]string{
		"COMMIT":      normalizeString(hash),
		"BRANCH":      normalizeString(branch),
		"AUTHOR":      normalizeString(author),
		"VERSION":     normalizeString(version),
		"BUILDTIME":   time.Now().Format("2006-01-02T15:04:05Z0700"),
		"GOARCH":      Goarch,
		"GOOS":        Goos,
		"CGO_ENABLED": "0",
	}
}

func isMn(r rune) bool {
	return unicode.Is(unicode.Mn, r)
}

func normalizeString(input string) string {
	t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
	result, _, _ := transform.String(t, input)
	result = strings.Replace(result, " ", "_", -1)
	return result
}

func Build() error {
   return sh.RunWith(flagEnv(), goexe, "build", "-ldflags", ldflags, "-o", "nazev_appky", "github.com/organizace/projekt/cmd/profil")
}

Fmt projektu

Každý správný projekt by měl mít všechny soubory správně formátované. Skript na to vypadá komplikovaně, ale věřím, že po chvilce to celé začne dávat smysl :-)

Hlavní část je “šumu” je vlastně jen listování souborů, které se mají poslat gofmt.

// importy si doplňte sami :-)

// vrací cesty package pro daný projekt jako slice řetězců
func packages() ([]string, error) {
	var err error
	pkgsInit.Do(func() {
		var s string
		s, err = sh.Output(goexe, "list", "./...")
		if err != nil {
			return
		}
		pkgs = strings.Split(s, "\n")
		for i := range pkgs {
			pkgs[i] = "." + pkgs[i][pkgPrefixLen:]
		}
	})
	return pkgs, err
}

// true pokud je go ve verzi 1.11 (kvůli `go mod`)
func isGoLatest() bool {
	return strings.Contains(runtime.Version(), "1.11")
}

// Run gofmt linter
func Fmt() error {
	if !isGoLatest() {
		return nil
	}
	pkgs, err := packages()
	if err != nil {
		return err
	}
	failed := false
	first := true
	for _, pkg := range pkgs {
		files, err := filepath.Glob(filepath.Join(pkg, "*.go"))
		if err != nil {
			return nil
		}
		for _, f := range files {
			s, err := sh.Output("gofmt", "-l", f)
			if err != nil {
				fmt.Printf("ERROR: running gofmt on %q: %v\n", f, err)
				failed = true
			}
			if s != "" {
				if first {
					fmt.Println("The following files are not gofmt'ed:")
					first = false
				}
				failed = true
				fmt.Println(s)
			}
		}
	}
	if failed {
		return errors.New("improperly formatted go files")
	}
	return nil
}

Testování

Bez toho to prostě nejde. Následující cíl (target) vám spustí testy a vypočítá nad nimi i pokrytí (coverage). Samozřejme race přepínač je nastavený (bez toho fakt nejde :-))

import "github.com/magefile/mage/sh"

func buildTags() string {
	if envtags := os.Getenv("VASE_APPKA_TAGY"); envtags != "" {
		return envtags
	}
	return "none"
}

// Run tests with race detector
func TestRace() error {
	return sh.Run(goexe, "test", "-race", "-coverprofile=coverage.txt", "-covermode=atomic", "-v", "-tags", buildTags(), "./...")
}

Vet

Mnohdy jde kód zkompilovat, ale jsou v něm skryté chyby, nesmysly apod. se kterými by prohram neměl opustit dveře vačí programátorské dílny (brlohu). go vet je najde a nahlásí. A vrátí chybu, pokud tomu tak je (z vlastní zkušenosti - 99% toho co najde prostě potřebuje opravit).

import "github.com/magefile/mage/sh"

//  Run go vet linter
func Vet() error {
	if err := sh.Run(goexe, "vet", "./..."); err != nil {
		return fmt.Errorf("error running go vet: %v", err)
	}
	return nil
}

Lint

Pokud nechcete použít go vet nebo chcete najít i další chyby a nesrovnalosti, můžete použít i jiné lintery. Zde je příklad golint.

import "github.com/magefile/mage/sh"

// Run golint linter
func Lint() error {
	// tato funkce je ukázána ve `Fmt`
   pkgs, err := packages()
	if err != nil {
		return err
	}
	failed := false
	for _, pkg := range pkgs {
		if _, err := sh.Exec(nil, os.Stderr, nil, "golint", pkg); err != nil {
			fmt.Printf("ERROR: running go lint on %q: %v\n", pkg, err)
			failed = true
		}
	}
	if failed {
		return errors.New("errors running golint")
	}
	return nil
}

Bonus

Jako bonus je ukázka jak porovnáním času modifikace můžete zjistit, jestli se vůbec něco změnilo a tudíž jestli je potřeba něco delat.

import "github.com/magefile/mage/target"

func isBuildNeeded() (bool, error) {
	return target.Dir("nazev_exace", "pkg", "cmd")
}

// Formats, Lints, Tests and Builds the application.
func Build() error {
	if needed, err := isBuildNeeded(); !needed && err == nil {
		log.Printf("Build not required.")
		return nil
   }
   ...
}

To je pro dnešek všechno, hodně zdaru při sestavování vašich aplikací. Na úplný závěr jeden můj magefile.go.

comments powered by Disqus