commit a5d76d8d5a76968c8e0dd27fdd169c674788a276 Author: Uday Hiwarale Date: Sun Apr 12 00:59:35 2020 +0530 release-v1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3b9203f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +# Debian GNU/Linux 10 (1.13.10-buster) +FROM golang:1.13.10-buster + +# copy entrypoint file +COPY entrypoint.go /usr/bin/entrypoint.go + +# change mode of the entrypoint file +RUN chmod +x /usr/bin/entrypoint.go + +# set entrypoint command +ENTRYPOINT [ "go", "run", "/usr/bin/entrypoint.go" ] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..39da8d4 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# GitHub Action: go-build-action +This actions generates cross-platform executable files from a Go module. + +![release](/assets/release.png) + +# Workflow setup + +```yaml +# workflow name +name: Generate release-artifacts + +# on events +on: + release: + types: + - created + +# workflow tasks +jobs: + generate: + name: Generate cross-platform builds + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + - name: Generate build files + uses: thatisuday/go-cross-build@v1 + with: + platforms: 'linux/amd64, darwin/amd64, windows/amd64' + package: 'demo' + name: 'program' + compress: 'true' + dest: 'dist' +``` + +### option: **platforms** +The `platforms` option specifies comma-separated platform names to create binary-executable files for. To see the list of supported platforms, use `go tool dist list` command. + +### option: **package** +If the module (_repository_) itself is a Go package, then `package` option value should be an empty string (''). If the repository contains a package directory, then `package` value should be the directory name. + +### option: **compress** +The `compress` option if set to `'true'` will generate **compressed-tar** archive files for the each platform-build file. The resulting archive file also contains `README.md` and `LICENSE` file if they exist inside the root of the repository. In this mode, the binary executable file name is taken from the `name` option value. + +### option: **name** +The `name` option sets a prefix for the build filenames. In compression mode, this prefix is applied to archive files and binary executable filename is set to this value. + +### option: **dest** +The `dest` option sets the output directory for the build files. This should be a relative directory without leading `./`. \ No newline at end of file diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..4423943 --- /dev/null +++ b/action.yml @@ -0,0 +1,43 @@ +# action name +name: 'go-cross-build' + +# action author +author: 'Uday Hiwarale ' + +# action description +description: 'Generates cross-platform executable files from a Go module.' + +# action input values +inputs: + platforms: + description: 'Comma-separated list of "/" combinations.' + default: 'linux/386,linux/amd64' + required: false + package: + description: 'Package (directory) in the module to build. By default, builds the module directory.' + default: '' + required: false + compress: + description: 'Compress each build file inside a ".tar.gz" archive.' + default: 'false' + required: false + name: + description: 'Binary executable filename and filenames prefix for the build files.' + default: 'program' + required: false + dest: + description: 'Destination directory inside workspace to output build-artifacts.' + default: 'build' + required: false + +# action runner (golang:latest image) +runs: + using: 'docker' + image: 'Dockerfile' + env: + GO111MODULE: 'on' + +# branding +branding: + icon: terminal + color: green diff --git a/assets/release.png b/assets/release.png new file mode 100644 index 0000000..8e194ae Binary files /dev/null and b/assets/release.png differ diff --git a/entrypoint.go b/entrypoint.go new file mode 100644 index 0000000..1d878ba --- /dev/null +++ b/entrypoint.go @@ -0,0 +1,215 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +/*************************************/ + +// check if file exists (relative to the current directory) +func fileExists(path string) bool { + + // get current directory + cwd, _ := os.Getwd() + + // get an absolute path of the file + absPath := filepath.Join(cwd, path) + + // access file information + if _, err := os.Stat(absPath); err != nil { + return !os.IsNotExist(err) // return `false` if doesn't exist + } + + // file exists + return true +} + +// copy file using `cp` command +func copyFile(src, dest string) { + if err := exec.Command("cp", src, dest).Run(); err != nil { + fmt.Println("An error occurred during copy operation:", src, "=>", dest) + os.Exit(1) + } +} + +/*************************************/ + +// build the package for a platform +func build(packageName, destDir string, platform map[string]string, compress bool) { + + // platform config + platformKernel := platform["kernel"] + platformArch := platform["arch"] + + // binary executable file path + inputName := os.Getenv("INPUT_NAME") + + // build file name (same as the `inputName` if compression is enabled) + buildFileName := fmt.Sprintf("%s-%s-%s", inputName, platformKernel, platformArch) + if compress { + buildFileName = inputName + } + + // append `.exe` file-extension for windows + if platformKernel == "windows" { + buildFileName += ".exe" + } + + // workspace directory + workspaceDir := os.Getenv("GITHUB_WORKSPACE") + + // destination directory path + destDirPath := filepath.Join(workspaceDir, destDir) + + // join destination path + buildFilePath := filepath.Join(destDirPath, buildFileName) + + // package directory local path + var packagePath string + if packageName == "" { + packagePath = "." + } else { + packagePath = "./" + packageName + } + + /*------------*/ + + // command-line options for the `go build` command + buildOptions := []string{"build", "-buildmode", "exe", "-o", buildFilePath, packagePath} + + // generate `go build` command + buildCmd := exec.Command("go", buildOptions...) + + // set environment variables + buildCmd.Env = append(os.Environ(), []string{ + fmt.Sprintf("GOOS=%s", platformKernel), + fmt.Sprintf("GOARCH=%s", platformArch), + }...) + + // execute `go build` command + fmt.Println("Creating a build using :", buildCmd.String()) + if output, err := buildCmd.Output(); err != nil { + fmt.Println("An error occurred during build:", err) + os.Exit(1) + } else { + fmt.Printf("%s\n", output) + } + + /*------------------------------*/ + + // create a compressed `.tar.gz` file + if compress { + + // compressed gzip file name + gzFileName := fmt.Sprintf("%s-%s-%s.tar.gz", inputName, platformKernel, platformArch) + + /*------------*/ + + // file to compress (default: build file) + includeFiles := []string{buildFileName} + + // copy "README.md" file inside destination directory + if fileExists("README.md") { + copyFile("README.md", filepath.Join(destDirPath, "README.md")) + includeFiles = append(includeFiles, "README.md") + } + + // copy "LICENSE" file inside destination directory + if fileExists("LICENSE") { + copyFile("LICENSE", filepath.Join(destDirPath, "LICENSE")) + includeFiles = append(includeFiles, "LICENSE") + } + + /*------------*/ + + // command-line options for the `tar` command + tarOptions := append([]string{"-cvzf", gzFileName}, includeFiles...) + + // generate `tar` command + tarCmd := exec.Command("tar", tarOptions...) + + // set working directory for the command + tarCmd.Dir = destDirPath + + // execute `tar` command + fmt.Println("Compressing build file using:", tarCmd.String()) + if err := tarCmd.Run(); err != nil { + fmt.Println("An error occurred during compression:", err) + os.Exit(1) + } + + /*------------*/ + + // generate cleanup command + cleanCmd := exec.Command("rm", append([]string{"-f"}, includeFiles...)...) + + // set working directory for the command + cleanCmd.Dir = destDirPath + + // start cleanup process + fmt.Println("Performing cleanup operation using:", cleanCmd.String()) + if err := cleanCmd.Run(); err != nil { + fmt.Println("An error occurred during cleaup:", err) + os.Exit(1) + } + + } +} + +/*************************************/ + +func main() { + + // get input variables from action + inputPlatforms := os.Getenv("INPUT_PLATFORMS") + inputPackage := os.Getenv("INPUT_PACKAGE") + inputCompress := os.Getenv("INPUT_COMPRESS") + inputDest := os.Getenv("INPUT_DEST") + + // package name to build + packageName := strings.ReplaceAll(inputPackage, " ", "") + + // destination directory + destDir := strings.ReplaceAll(inputDest, " ", "") + + // split platform names by comma (`,`) + platforms := strings.Split(inputPlatforms, ",") + + // should compress build file + compress := false + if strings.ToLower(inputCompress) == "true" { + compress = true + } + + // for each platform, execute `build` function + for _, platform := range platforms { + + // split platform by `/` (and clean all whitespaces) + platformSpec := strings.Split(strings.ReplaceAll(platform, " ", ""), "/") + + // create a `map` of `kernel` and `arch` + platformMap := map[string]string{ + "kernel": platformSpec[0], + "arch": platformSpec[1], + } + + // execute `build` function + build(packageName, destDir, platformMap, compress) + } + + /*------------*/ + + // list files inside destination directory + if output, err := exec.Command("ls", "-alh", destDir).Output(); err != nil { + fmt.Println("An error occurred during ls operation:", err) + os.Exit(1) + } else { + fmt.Println("--- BUILD FILES ---") + fmt.Printf("%s\n", output) + } + +}