Compare commits

...

13 Commits
v1.0.0 ... main

Author SHA1 Message Date
Timon Ringwald 20f84c6bf1 print go install command by default 2022-08-30 00:42:47 +02:00
Timon Ringwald c964a85673 show build info 2022-08-29 10:50:38 +02:00
Timon Ringwald f91780d6ce insert buildinfo at compile time 2022-08-29 10:42:18 +02:00
Timon Ringwald 402b269bf4 removed commented code 2022-08-29 00:41:27 +02:00
Timon Ringwald 8ec8d32a5b remove debug flags by default 2022-08-29 00:03:56 +02:00
Timon Ringwald bd0ed87f70 updated README 2022-08-18 15:06:39 +02:00
Timon Ringwald 9569739605 new chowcase for v1.0.3 added 2022-08-18 00:01:01 +02:00
Timon Ringwald 3c0bc4f1b0 added -v 2022-08-17 23:56:34 +02:00
Timon Ringwald 8616fe67dc show individual time 2022-08-17 23:38:41 +02:00
Timon Ringwald 2027f3412c live demo added 2022-08-17 23:21:17 +02:00
Timon Ringwald ab7f840215 fixed buggy error message 2022-08-17 22:55:17 +02:00
Timon Ringwald 115871aeb7 fixed empty config file 2022-08-17 22:40:39 +02:00
Timon Ringwald 252d0b7278 added various new arguments + LICENSE and README 2022-08-17 22:24:13 +02:00
12 changed files with 478 additions and 52 deletions

19
LICENSE.md Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2022 Mila Ringwald
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

140
README.md Normal file
View File

@ -0,0 +1,140 @@
# gocc
A high-performance cross compiler for Go
## Source code
You can find the source code here: [git.milar.in](https://git.milar.in/milarin/gocc)
## Installation
If you have Go installed, you can simply go install the program: `go install git.milar.in/milarin/gocc@latest`
There are pre-compiled executables for various platforms on the [repository](https://git.milar.in/milarin/gocc/releases).
## License
Distributed under the MIT License. See [LICENSE.md](https://git.milar.in/milarin/gocc/src/branch/main/LICENSE.md)
## Demo
[![asciicast](https://asciinema.org/a/YOFZE4hskODO5TkNedyIBa4V8.svg)](https://asciinema.org/a/YOFZE4hskODO5TkNedyIBa4V8)
## Usage
### Basic syntax
`gocc [<arguments>] [<module>]`
### Arguments
Using `--help` shows a description for all available arguments:
```sh
$ gocc --help
Usage of ./gocc:
-arch string
comma-separated list of architectures to compile for (empty for all)
-c dont compress any executables
-f string
go template for filenames (default "{{.Name}}-{{.OS}}-{{.Arch}}{{.Ext}}")
-findconfig
print config file path and exit
-ignoreconfig
dont read any config file
-o string
output directory (default "output")
-os string
comma-separated list of operating systems to compile for (empty for all)
-s silent mode (no output)
-saveconfig
save config file with current configuration and exit
-t int
amount of threads (0 = infinite) (default 16)
-v show version and exit
```
#### Providing a Go module
By default, `gocc` compiles the module in the current working directory.
You can provide a custom module path after all other arguments.
Example: `gocc -t 4 -ignoreconfig path/to/go/module`
#### Choosing Operating systems
You can decide, for which operating systems `gocc` should compile for via `-os`.
Provide the operating systems as a comma-separated list.
Example: `gocc -os "windows,linux"`
For a list of supported operating systems, run `go tool dist list`.
Providing no `-os` flag will compile for all available systems (without config file)
#### Choosing architectures
Analogous to operating systems, you can provide architectures via `-arch`.
Example: `gocc -arch "amd64,386"`
For a list of supported architectures, run `go tool dist list`.
Providing no `-arch` flag will compile for all available architectures (without config file)
#### Output path
By default, `gocc` will save all executables in the folder `output` in the current working directory.
You can set a custom path via `-o`
#### Disable compression
All executables will be compressed automatically if `upx` is found in `$PATH`.
This behavior can be disabled via `-c`.
#### Multithreaded compilation
Compilation is multi-threaded by default. You can set the amount of concurrent compilations via `-t <threads>`.
If no thread count is provided, the amount of CPU cores will be used.
(the default value shown with `--help` is always the amount of CPU cores on the current machine)
A thread count of `0` runs all tasks at the same time. This could lead to lags and freezes on your machine!
If you want to disable multi-threaded compilation, just use `-t 1`
#### Silent mode
Providing `-s` enables silent mode. When enabled, `gocc` will never show any output.
A non-zero exit code indicates errors.
#### Customize default behavior
You can change the default values of all other arguments with `-saveconfig`.
Simply provide all arguments as you like and save your config in a json file.
Next time `gocc` is run, the config file will be loaded and your preferred command line arguments will be automatically set.
You can still manually overwrite arguments by providing them directly.
But that will not always result in the default behavior if no config file exists.
For these cases you can use `-ignoreconfig`.
If you want to get rid of your config file or you want to edit it manually, you can find it by running `gocc -findconfig`
**No compilation will be done if `-saveconfig` is provided!**
**`-findconfig` does not work with silent mode (`-s`)!**
**Always use `-ignoreconfig` in scripts! You don't know what config file your user might have!**
#### Change executable filename pattern
You can provide a custom filename pattern for the compiled executables via `-f`.
The value feeded into `-f` is a Go template.
See the official Go docs for [Go templates](https://pkg.go.dev/text/template) for more information.
The following values are available inside the template:
- `Name`: the module name
- `OS`: the operating system
- `Arch`: the architecture
- `Ext`: the file extension (for windows: `.exe`, for any other operating system: ``)
The default value is `{{.Name}}-{{.OS}}-{{.Arch}}{{.Ext}}`

View File

@ -1,14 +1,30 @@
package main
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
)
func Compile(cfg *CompileConfig, ch chan<- *CompileReport, wg *sync.WaitGroup) {
filePath := filepath.Join(*OutputDir, fmt.Sprintf("%s_%s_%s%s", ProjectName, cfg.OS, cfg.Arch, cfg.FileExt))
start := time.Now()
// calculate filepath
b := new(strings.Builder)
data := &OutputFileTmplData{
Name: ProjectName,
OS: cfg.OS,
Arch: cfg.Arch,
Ext: cfg.FileExt,
}
if err := OutputFileTmpl.Execute(b, data); err != nil {
ch <- &CompileReport{Config: cfg, State: StateCompileError}
wg.Done()
return
}
filePath := filepath.Join(*OutputDir, b.String())
filePath, err := filepath.Abs(filePath)
if err != nil {
@ -19,7 +35,9 @@ func Compile(cfg *CompileConfig, ch chan<- *CompileReport, wg *sync.WaitGroup) {
ch <- &CompileReport{Config: cfg, State: StateCompiling}
compileCmd := exec.Command("go", "build", "-o", filePath)
args := []string{"build", "-o", filePath, "-ldflags=" + BuildLdFlags(cfg.OS, cfg.Arch)}
compileCmd := exec.Command("go", args...)
compileCmd.Dir = ModulePath
if err := compileCmd.Start(); err != nil {
@ -32,17 +50,11 @@ func Compile(cfg *CompileConfig, ch chan<- *CompileReport, wg *sync.WaitGroup) {
wg.Done()
return
}
Compress(filePath, cfg, ch, wg)
// uncomment for independent compile and compress tasks
// (slightly slower in tests, most likely because of more context switches)
/*
ch <- &CompileReport{Config: cfg, State: StateWaiting}
go Runner.Run(func() { Compress(filePath, cfg, ch, wg) })
*/
Compress(filePath, start, cfg, ch, wg)
}
func Compress(filePath string, cfg *CompileConfig, ch chan<- *CompileReport, wg *sync.WaitGroup) {
func Compress(filePath string, start time.Time, cfg *CompileConfig, ch chan<- *CompileReport, wg *sync.WaitGroup) {
defer wg.Done()
ch <- &CompileReport{Config: cfg, State: StateCompressing}
@ -62,5 +74,5 @@ func Compress(filePath string, cfg *CompileConfig, ch chan<- *CompileReport, wg
}
}
ch <- &CompileReport{Config: cfg, State: StateDone}
ch <- &CompileReport{Config: cfg, State: StateDone, Duration: time.Since(start)}
}

View File

@ -1,12 +1,15 @@
package main
import (
"time"
"github.com/fatih/color"
)
type CompileReport struct {
Config *CompileConfig
State CompileState
Config *CompileConfig
State CompileState
Duration time.Duration
}
type CompileState string

View File

@ -2,6 +2,8 @@ package main
import (
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"strings"
@ -10,14 +12,22 @@ import (
)
type Config struct {
OutputDir string `json:"output_dir"`
OutputDir string `json:"output_dir"`
OutputFile string `json:"output_file"`
OS []string `json:"os"`
Arch []string `json:"arch"`
Silent bool `json:"silent"`
NoCompress bool `json:"no_compress"`
NumThreads int `json:"num_threads"`
Silent bool `json:"silent"`
NoCompress bool `json:"no_compress"`
NumThreads int `json:"num_threads"`
KeepDebugFlags bool `json:"debug_flags"`
}
func SetFlagIfDefault[T comparable](flag *T, newValue T, defaultValue T) {
if *flag == defaultValue {
*flag = newValue
}
}
func LoadConfig() error {
@ -34,15 +44,21 @@ func LoadConfig() error {
cfg := &Config{}
if err := json.NewDecoder(file).Decode(cfg); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
*OutputDir = cfg.OutputDir
*OS = strings.Join(cfg.OS, ",")
*Arch = strings.Join(cfg.Arch, ",")
*Silent = cfg.Silent
*NoCompress = cfg.NoCompress
*NumThreads = cfg.NumThreads
SetFlagIfDefault(OutputDir, cfg.OutputDir, DefaultOutputDir)
SetFlagIfDefault(OutputFile, cfg.OutputFile, DefaultOutputFile)
SetFlagIfDefault(OS, strings.Join(cfg.OS, ","), DefaultOS)
SetFlagIfDefault(Arch, strings.Join(cfg.Arch, ","), DefaultArch)
SetFlagIfDefault(Silent, cfg.Silent, DefaultSilent)
SetFlagIfDefault(NoCompress, cfg.NoCompress, DefaultNoCompress)
SetFlagIfDefault(NumThreads, cfg.NumThreads, DefaultNumThreads)
SetFlagIfDefault(KeepDebugFlags, cfg.KeepDebugFlags, DefaultKeepDebugFlags)
return nil
}
@ -66,12 +82,14 @@ func SaveConfig() (string, error) {
enc.SetIndent("", "\t")
cfg := &Config{
OutputDir: *OutputDir,
OS: strings.Split(*OS, ","),
Arch: strings.Split(*Arch, ","),
Silent: *Silent,
NoCompress: *NoCompress,
NumThreads: *NumThreads,
OutputDir: *OutputDir,
OutputFile: *OutputFile,
OS: strings.Split(*OS, ","),
Arch: strings.Split(*Arch, ","),
Silent: *Silent,
NoCompress: *NoCompress,
NumThreads: *NumThreads,
KeepDebugFlags: *KeepDebugFlags,
}
if err := enc.Encode(cfg); err != nil {

1
go.mod
View File

@ -3,6 +3,7 @@ module git.milar.in/milarin/gocc
go 1.19
require (
git.milar.in/milarin/buildinfo v1.0.0
git.milar.in/milarin/channel v0.0.7
git.milar.in/milarin/configfile v1.0.2
git.milar.in/milarin/gmath v0.0.1

2
go.sum
View File

@ -1,3 +1,5 @@
git.milar.in/milarin/buildinfo v1.0.0 h1:tw98GupUYl/0a/3aPGuezhE4wseycOSsbcLp70hy60U=
git.milar.in/milarin/buildinfo v1.0.0/go.mod h1:arI9ZoENOgcZcanv25k9y4dKDUhPp0buJrlVerGruas=
git.milar.in/milarin/channel v0.0.7 h1:cVKtwgH/EE7U+XTHcoFCClJ4LR349KanzjX9xKwRcNg=
git.milar.in/milarin/channel v0.0.7/go.mod h1:We83LTI8S7u7II3pD+A2ChCDWJfCkcBUCUqii9HjTtM=
git.milar.in/milarin/configfile v1.0.2 h1:QgZVSVDsFm3HK7PEg6a2ANeZxqo0JlIooyyJv8VaCNI=

21
init.go
View File

@ -7,12 +7,27 @@ import (
"os"
"os/exec"
"path/filepath"
"git.milar.in/milarin/buildinfo"
"git.milar.in/milarin/configfile"
)
func Init() {
flag.Parse()
var err error
if *ShowVersion {
buildinfo.Print(buildinfo.Options{})
os.Exit(0)
}
if *FindConfigFile {
if configFilePath, err := configfile.Path("json"); err == nil {
fmt.Println(configFilePath)
os.Exit(0)
}
}
if *SaveConfigFile {
if configFilePath, err := SaveConfig(); err == nil {
Println(ColorDone.Sprintf("config file saved at '%s'", configFilePath))
@ -21,12 +36,14 @@ func Init() {
Println(ColorError.Sprint(fmt.Errorf("saving config file failed: %w", err)))
os.Exit(1)
}
} else {
} else if !*IgnoreConfigFile {
if err := LoadConfig(); err != nil {
Println(ColorError.Sprint(fmt.Errorf("loading config file failed: %w", err)))
}
}
OutputFileTmpl.Parse(*OutputFile)
ModulePath, err = filepath.Abs(flag.Arg(0))
if err != nil {
Println(ColorError.Sprint("determining module path failed"))
@ -71,7 +88,7 @@ func Init() {
if !*NoCompress {
if _, err := exec.LookPath("upx"); err != nil {
Println(ColorWarn.Sprint(os.Stderr, "upx not found in PATH. file compression not possible"))
Println(ColorWarn.Sprint("upx not found in PATH. file compression not possible"))
*NoCompress = true
}
}

63
main.go
View File

@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"sync"
"text/template"
"time"
"git.milar.in/milarin/channel"
@ -17,39 +18,69 @@ import (
// globals
var (
ModulePath string
OutputFileTmpl = template.New("output-file")
ModulePath string
ProjectPath string
ProjectName string
Runner channel.Runner
// meta data for executables
VersionTag string
CommitHash string
BuildTime string
Runner channel.Runner
MaxConfigStringLength int
)
// command line arguments default values
var (
DefaultOutputDir = "output"
DefaultOS = ""
DefaultArch = ""
DefaultSilent = false
DefaultNoCompress = false
DefaultNumThreads = runtime.NumCPU()
DefaultOutputDir = "output"
DefaultOutputFile = "{{.Name}}-{{.OS}}-{{.Arch}}{{.Ext}}"
DefaultOS = ""
DefaultArch = ""
DefaultSilent = false
DefaultNoCompress = false
DefaultNumThreads = runtime.NumCPU()
DefaultKeepDebugFlags = false
)
// command line arguments
var (
OutputDir = flag.String("o", DefaultOutputDir, "output directory")
OutputDir = flag.String("o", DefaultOutputDir, "output directory")
OutputFile = flag.String("f", DefaultOutputFile, "go template for filenames")
OS = flag.String("os", DefaultOS, "comma-separated list of operating systems to compile for (empty for all)")
Arch = flag.String("arch", DefaultArch, "comma-separated list of architectures to compile for (empty for all)")
Silent = flag.Bool("s", DefaultSilent, "silent mode (no output)")
NoCompress = flag.Bool("c", DefaultNoCompress, "dont compress any executables")
NumThreads = flag.Int("t", DefaultNumThreads, "amount of threads (0 = infinite)")
Silent = flag.Bool("s", DefaultSilent, "silent mode (no output)")
NoCompress = flag.Bool("c", DefaultNoCompress, "dont compress any executables")
NumThreads = flag.Int("t", DefaultNumThreads, "amount of threads (0 = infinite)")
KeepDebugFlags = flag.Bool("d", DefaultKeepDebugFlags, "keep debug flags")
SaveConfigFile = flag.Bool("saveconfig", false, "save config file with current configuration and exit")
SaveConfigFile = flag.Bool("saveconfig", false, "save config file with current configuration and exit")
IgnoreConfigFile = flag.Bool("ignoreconfig", false, "dont read any config file")
FindConfigFile = flag.Bool("findconfig", false, "print config file path and exit")
DontPrintInstallCmd = flag.Bool("p", false, "print 'go install' command")
ShowVersion = flag.Bool("v", false, "show version and exit")
)
func main() {
Init()
GatherMetaData()
if !*Silent && !*DontPrintInstallCmd {
tag := VersionTag
if tag == "" || strings.HasPrefix(tag, "(devel") {
tag = "latest"
}
Println(ColorDone.Sprintf("go install -ldflags=\"%s\" %s@%s", BuildLdFlags("", ""), ProjectPath, tag))
Println()
}
ch := make(chan *CompileReport, len(CompileConfigs))
wg := new(sync.WaitGroup)
@ -75,7 +106,7 @@ func main() {
<-doneCh // wait for Watch routine
if !*Silent {
fmt.Printf("compilation took %s (using %s threads)\n", end.Sub(start), GetThreadCountString())
fmt.Printf("compilation took %s (using %s threads)\n", end.Sub(start).Truncate(time.Second/100), GetThreadCountString())
}
}
@ -87,8 +118,8 @@ func DetermineProjectName() error {
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "module ") {
fullName := strings.TrimPrefix(line, "module ")
parts := strings.Split(fullName, "/")
ProjectPath = strings.TrimPrefix(line, "module ")
parts := strings.Split(ProjectPath, "/")
ProjectName = parts[len(parts)-1]
return nil
}

165
metadata.go Normal file
View File

@ -0,0 +1,165 @@
package main
import (
"errors"
"fmt"
"io"
"os/exec"
"strings"
"time"
)
func GetCommitHash(hashfmt string) (string, error) {
gitCmd := exec.Command("git", "log", "-n", "1", "--pretty=format:"+hashfmt)
gitCmd.Dir = ModulePath
stdout, err := gitCmd.StdoutPipe()
if err != nil {
return "", err
}
if err := gitCmd.Start(); err != nil {
return "", err
}
hashData, err := io.ReadAll(stdout)
if err != nil {
return "", err
}
if err := gitCmd.Wait(); err != nil {
return "", err
}
trimmedHash := strings.TrimSpace(string(hashData))
if trimmedHash == "" {
return "", errors.New("no valid hash found")
}
return trimmedHash, nil
}
func GetVersionTag() (string, error) {
gitCmd := exec.Command("git", "log", "-n", "1", "--pretty=format:%(describe:tags)")
gitCmd.Dir = ModulePath
stdout, err := gitCmd.StdoutPipe()
if err != nil {
return "", err
}
if err := gitCmd.Start(); err != nil {
return "", err
}
tagData, err := io.ReadAll(stdout)
if err != nil {
return "", err
}
if err := gitCmd.Wait(); err != nil {
return "", err
}
trimmedTag := strings.TrimSpace(string(tagData))
if trimmedTag == "" {
return "", errors.New("no valid version tag found")
}
return trimmedTag, nil
}
func WorkTreeChanged() bool {
gitCmd := exec.Command("git", "status", "--porcelain")
gitCmd.Dir = ModulePath
stdout, err := gitCmd.StdoutPipe()
if err != nil {
return false
}
if err := gitCmd.Start(); err != nil {
return false
}
data, err := io.ReadAll(stdout)
if err != nil {
return false
}
if err := gitCmd.Wait(); err != nil {
return false
}
return len(data) != 0
}
func GatherMetaData() {
BuildTime = time.Now().Format(time.RFC3339)
if !WorkTreeChanged() {
VersionTag, _ = GetVersionTag()
CommitHash, _ = GetCommitHash("%H")
} else {
hash, _ := GetCommitHash("%h")
VersionTag = fmt.Sprintf("(devel-%s)", hash)
}
if !*Silent {
ColorDone.Println("build info:")
fmt.Printf("version: %s\n", VersionTag)
fmt.Printf("build time: %s\n", BuildTime)
if CommitHash != "" {
fmt.Printf("commit hash: %s\n", CommitHash)
}
fmt.Println()
}
}
func BuildLdFlags(os, arch string) string {
b := &strings.Builder{}
if !*KeepDebugFlags {
b.WriteString("-s -w")
}
if CommitHash != "" {
b.WriteString(" -X ")
b.WriteString("git.milar.in/milarin/buildinfo.Commit")
b.WriteString("=")
b.WriteString(CommitHash)
}
if VersionTag != "" {
b.WriteString(" -X ")
b.WriteString("git.milar.in/milarin/buildinfo.Version")
b.WriteString("=")
b.WriteString(VersionTag)
}
b.WriteString(" -X ")
b.WriteString("git.milar.in/milarin/buildinfo.Name")
b.WriteString("=")
b.WriteString(ProjectName)
b.WriteString(" -X ")
b.WriteString("git.milar.in/milarin/buildinfo.BuildTime")
b.WriteString("=")
b.WriteString(BuildTime)
if os != "" {
b.WriteString(" -X ")
b.WriteString("git.milar.in/milarin/buildinfo.OS")
b.WriteString("=")
b.WriteString(os)
}
if arch != "" {
b.WriteString(" -X ")
b.WriteString("git.milar.in/milarin/buildinfo.Arch")
b.WriteString("=")
b.WriteString(arch)
}
return b.String()
}

8
output_file_data.go Normal file
View File

@ -0,0 +1,8 @@
package main
type OutputFileTmplData struct {
Name string
OS string
Arch string
Ext string
}

View File

@ -1,10 +1,15 @@
package main
import "fmt"
import (
"fmt"
"time"
)
func Watch(ch <-chan *CompileReport, doneCh chan<- struct{}) {
defer close(doneCh)
times := map[*CompileConfig]time.Duration{}
states := map[*CompileConfig]CompileState{}
for _, cfg := range CompileConfigs {
states[cfg] = StateWaiting
@ -14,8 +19,9 @@ func Watch(ch <-chan *CompileReport, doneCh chan<- struct{}) {
for report := range ch {
states[report.Config] = report.State
times[report.Config] = report.Duration.Truncate(time.Second / 100)
if !*Silent {
PrintStates(states, cleared)
PrintStates(states, times, cleared)
}
cleared = true
}
@ -23,7 +29,7 @@ func Watch(ch <-chan *CompileReport, doneCh chan<- struct{}) {
doneCh <- struct{}{}
}
func PrintStates(states map[*CompileConfig]CompileState, clear bool) {
func PrintStates(states map[*CompileConfig]CompileState, times map[*CompileConfig]time.Duration, clear bool) {
if clear {
for i := 0; i < len(CompileConfigs); i++ {
goUp()
@ -38,6 +44,10 @@ func PrintStates(states map[*CompileConfig]CompileState, clear bool) {
continue
}
fmt.Printf(fmt.Sprintf("%%-%ds %%s\n", MaxConfigStringLength), cfg.String(), state)
if state == StateDone {
fmt.Printf(fmt.Sprintf("%%-%ds %%s (%%s)\n", MaxConfigStringLength), cfg.String(), state, times[cfg])
} else {
fmt.Printf(fmt.Sprintf("%%-%ds %%s\n", MaxConfigStringLength), cfg.String(), state)
}
}
}