Logging & Godot Downloader

This commit is contained in:
Layla 2023-07-23 02:30:27 -04:00
parent bc33b4805a
commit 649d0d87f9
11 changed files with 728 additions and 0 deletions

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM golang:1-alpine3.18 AS builder
RUN go build main.go -o /usr/local/bin/godot-build-tool
FROM alpine:3.18
RUN apk add --no-cache unzip
RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub \
&& wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r1/glibc-2.35-r1.apk \
&& apk add glibc-2.35-r1.apk
WORKDIR /opt
COPY --from=builder /usr/local/bin/godot-build-tool /opt/godot-build-tool
CMD ["/opt/godot-build-tool"]

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module github.com/yeslayla/godot-build-tools
go 1.20
require (
github.com/sethvargo/go-envconfig v0.9.0 // indirect
github.com/sethvargo/go-githubactions v1.1.0 // indirect
)

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE=
github.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0=
github.com/sethvargo/go-githubactions v1.1.0 h1:mg03w+b+/s5SMS298/2G6tHv8P0w0VhUFaqL1THIqzY=
github.com/sethvargo/go-githubactions v1.1.0/go.mod h1:qIboSF7yq2Qnaw2WXDsqCReM0Lo1gU4QXUWmhBC3pxE=

160
internal/downloader.go Normal file
View File

@ -0,0 +1,160 @@
package internal
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"github.com/yeslayla/godot-build-tools/logging"
"github.com/yeslayla/godot-build-tools/utils"
)
type DownloaderOptions struct {
DownloadRepositoryURL string
BinDir string
}
type Downloader struct {
downloadRepositoryURL string
bin string
logger logging.Logger
}
func NewDownloader(targetOS TargetOS, logger logging.Logger, options *DownloaderOptions) *Downloader {
var url string = options.DownloadRepositoryURL
if url == "" {
url = "https://downloads.tuxfamily.org/godotengine/"
}
var binDir string = options.BinDir
if binDir == "" {
switch targetOS {
case TargetOSLinux:
home, _ := os.UserHomeDir()
binDir = filepath.Join(home, "/.local/bin")
case TargetOSWindows:
binDir = "C:\\Program Files (x86)\\Godot"
case TargetOSMacOS:
binDir = "/Applications/Godot"
}
}
return &Downloader{
downloadRepositoryURL: url,
bin: binDir,
logger: logger,
}
}
func getRemoteFileFormat(targetOS TargetOS, version string) string {
switch targetOS {
case TargetOSLinux:
if version[0] == '3' {
return "Godot_v%s-%s_x11.64.zip"
}
return "Godot_v%s-%s_linux.x86_64.zip"
case TargetOSWindows:
return "Godot_v%s-%s_win64.exe.zip"
case TargetOSMacOS:
return "Godot_v%s-%s_macos.universal.zip"
}
return ""
}
func (d *Downloader) DownloadGodot(targetOS TargetOS, version string, release string) (string, error) {
var fileName string = fmt.Sprintf(getRemoteFileFormat(targetOS, version), version, release)
tempDir, _ := os.MkdirTemp("", "godot-build-tools")
outFile := filepath.Join(tempDir, fileName)
out, err := os.Create(outFile)
if err != nil {
return "", fmt.Errorf("failed to create output file: %s", err)
}
defer out.Close()
downloadURL, err := url.Parse(d.downloadRepositoryURL)
if err != nil {
return "", fmt.Errorf("failed to parse download repository URL: %s", err)
}
downloadURL.Path = path.Join(downloadURL.Path, version)
if release != "stable" {
downloadURL.Path = path.Join(downloadURL.Path, release)
}
downloadURL.Path = path.Join(downloadURL.Path, fileName)
d.logger.Debugf("Download URL: %s", downloadURL.String())
resp, err := http.Get(downloadURL.String())
if err != nil {
return "", fmt.Errorf("failed to download Godot: %s", err)
}
defer resp.Body.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return "", fmt.Errorf("failed to write Godot package to output file: %s", err)
}
return outFile, nil
}
func (d *Downloader) UnzipGodot(targetOS TargetOS, godotPackage string) (string, error) {
files, err := utils.Unzip(godotPackage)
if err != nil {
return "", fmt.Errorf("failed to unzip Godot package: %s", err)
}
// Look for godot binary
for _, file := range files {
switch targetOS {
case TargetOSLinux:
if path.Ext(file) == ".x86_64" || path.Ext(file) == ".64" {
return file, nil
}
case TargetOSWindows:
if path.Ext(file) == ".exe" {
return file, nil
}
case TargetOSMacOS:
if path.Ext(file) == ".universal" {
return file, nil
}
}
}
return "", fmt.Errorf("failed to find godot binary in Godot package")
}
func (d *Downloader) InstallGodot(godotPackage string, targetOS TargetOS, version string, release string) (string, error) {
// Unzip package
godotUnzipBinPath, err := d.UnzipGodot(targetOS, godotPackage)
if err != nil {
return "", fmt.Errorf("failed to unzip Godot package: %s", err)
}
godotBin := path.Base(godotUnzipBinPath)
godotBinPath := filepath.Join(d.bin, godotBin)
// Copy Godot binary to bin directory
data, err := ioutil.ReadFile(godotUnzipBinPath)
if err != nil {
return "", fmt.Errorf("failed to read Godot binary: %s", err)
}
err = ioutil.WriteFile(godotBinPath, data, 0755)
if err != nil {
return "", fmt.Errorf("failed to write Godot binary: %s", err)
}
_ = os.Remove(godotUnzipBinPath)
return godotBinPath, nil
}

56
internal/godot4.go Normal file
View File

@ -0,0 +1,56 @@
package internal
import "strings"
type Godot4ArgBuilder struct {
args []string
}
func NewGodot4ArgBuilder(projectDir string) GodotArgBuilder {
return &Godot4ArgBuilder{
args: []string{"--path", projectDir},
}
}
func (b *Godot4ArgBuilder) AddHeadlessFlag() {
b.args = append(b.args, "--headless")
}
func (b *Godot4ArgBuilder) AddDebugFlag() {
b.args = append(b.args, "--debug")
}
func (b *Godot4ArgBuilder) AddVerboseFlag() {
b.args = append(b.args, "--verbose")
}
func (b *Godot4ArgBuilder) AddQuietFlag() {
b.args = append(b.args, "--quiet")
}
func (b *Godot4ArgBuilder) AddDumpGDExtensionInterfaceFlag() {
b.args = append(b.args, "--dump-gdextension-interface")
}
func (b *Godot4ArgBuilder) AddDumpExtensionApiFlag() {
b.args = append(b.args, "--dump-extension-api")
}
func (b *Godot4ArgBuilder) AddCheckOnlyFlag() {
b.args = append(b.args, "--check-only")
}
func (b *Godot4ArgBuilder) AddExportFlag(exportType ExportType) {
switch exportType {
case ExportTypeRelease:
b.args = append(b.args, "--export")
case ExportTypeDebug:
b.args = append(b.args, "--export-debug")
case ExportTypePack:
b.args = append(b.args, "--export-pack")
}
}
func (b *Godot4ArgBuilder) GenerateArgs() string {
return strings.Join(b.args, " ")
}

24
internal/godot_wrapper.go Normal file
View File

@ -0,0 +1,24 @@
package internal
type ExportType uint8
const (
ExportTypeRelease ExportType = iota
ExportTypeDebug
ExportTypePack
)
type GodotArgBuilder interface {
AddHeadlessFlag()
AddDebugFlag()
AddVerboseFlag()
AddQuietFlag()
AddDumpGDExtensionInterfaceFlag()
AddDumpExtensionApiFlag()
AddCheckOnlyFlag()
AddExportFlag(exportType ExportType)
GenerateArgs() string
}

9
internal/os.go Normal file
View File

@ -0,0 +1,9 @@
package internal
type TargetOS uint8
const (
TargetOSLinux TargetOS = iota
TargetOSWindows
TargetOSMacOS
)

136
logging/github_actions.go Normal file
View File

@ -0,0 +1,136 @@
package logging
import (
"fmt"
"log"
"os"
)
// GitHubActionsLogger is a logger that logs to GitHub Actions.
type GitHubActionsLogger struct {
info *log.Logger
warn *log.Logger
err *log.Logger
debug *log.Logger
}
// NewGitHubActionsLogger creates a new GitHubActionsLogger.
func NewGitHubActionsLogger(debug bool) Logger {
var debugLogger *log.Logger
if debug {
debugLogger = log.New(os.Stdout, "::debug::", 0)
}
return &GitHubActionsLogger{
info: log.New(os.Stdout, "", 0),
warn: log.New(os.Stdout, "::warning::", 0),
err: log.New(os.Stderr, "::error::", 0),
debug: debugLogger,
}
}
// Infof logs an info message.
func (l *GitHubActionsLogger) Infof(format string, args ...interface{}) {
l.info.Printf(format, args...)
}
// Warnf logs a warning message.
func (l *GitHubActionsLogger) Warnf(format string, args ...interface{}) {
l.warn.Printf(format, args...)
}
// Errorf logs an error message.
func (l *GitHubActionsLogger) Errorf(format string, args ...interface{}) {
l.err.Printf(format, args...)
}
// Debugf logs a debug message if debug logging is enabled.
func (l *GitHubActionsLogger) Debugf(format string, args ...interface{}) {
if l.debug != nil {
l.debug.Printf(format, args...)
}
}
// NoticeMessage sends a notice message to GitHub Actions.
func (l *GitHubActionsLogger) NoticeMessage(message string, input NoticeMessageInput) {
var prefix string = "::notice"
if input.Title != nil {
prefix += " title=" + *input.Title
}
if input.Filename != nil {
prefix += " file=" + *input.Filename
}
if input.Line != nil {
prefix += " line=" + fmt.Sprint(*input.Line)
}
if input.EndLine != nil {
prefix += " endLine=" + fmt.Sprint(*input.EndLine)
}
if input.Col != nil {
prefix += " col=" + fmt.Sprint(*input.Col)
}
if input.EndCol != nil {
prefix += " endColumn=" + fmt.Sprint(*input.EndCol)
}
l.info.Printf("%s::%s", prefix, message)
}
// StartGroup groups together log messages.
func (l *GitHubActionsLogger) StartGroup(name string) {
l.info.Printf("::group::%s", name)
}
// EndGroup ends a group.
func (l *GitHubActionsLogger) EndGroup() {
l.info.Println("::endgroup::")
}
// Mask masks a value in log output.
func (l *GitHubActionsLogger) Mask(value string) {
l.info.Printf("::add-mask::%s", value)
}
// SetOutput sets an output parameter.
func (l *GitHubActionsLogger) SetOutput(name string, value string) {
outputFile := os.Getenv("GITHUB_OUTPUT")
if outputFile == "" {
l.Errorf("GITHUB_OUTPUT is not set")
return
}
f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
l.Errorf("failed to open output file: %v", err)
return
}
defer f.Close()
if _, err := f.WriteString(fmt.Sprint(name, "=", value)); err != nil {
l.Errorf("failed to write output file: %v", err)
return
}
}
// SetSummary sets a job's summary in markdown format.
func (l *GitHubActionsLogger) SetSummary(summary string) {
summaryFile := os.Getenv("GITHUB_STEP_SUMMARY")
if summaryFile == "" {
l.Errorf("GITHUB_STEP_SUMMARY is not set")
return
}
f, err := os.OpenFile(summaryFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
l.Errorf("failed to open summary file: %v", err)
return
}
defer f.Close()
if _, err := f.WriteString(summary); err != nil {
l.Errorf("failed to write summary file: %v", err)
return
}
}

202
logging/logger.go Normal file
View File

@ -0,0 +1,202 @@
package logging
import (
"fmt"
"log"
"os"
"strings"
)
// Logger is an interface for logging.
type Logger interface {
Infof(format string, args ...interface{})
Warnf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Debugf(format string, args ...interface{})
Mask(value string)
StartGroup(name string)
EndGroup()
NoticeMessage(message string, input NoticeMessageInput)
SetOutput(name string, value string)
SetSummary(summary string)
}
// DefaultLogger is a logger that logs to the console.
type DefaultLogger struct {
info *log.Logger
warn *log.Logger
err *log.Logger
debug *log.Logger
outputsFile string
summaryFile string
groups []string
masks []string
}
// NoticeMessageInput holds optional parameters for a notice message.
type NoticeMessageInput struct {
Title *string
Filename *string
Line *int
EndLine *int
Col *int
EndCol *int
}
// LoggerOptions holds options for creating a new logger.
type LoggerOptions struct {
OutputsFile string
SummaryFile string
Debug bool
}
// NewLogger creates a new default logger.
func NewLogger(options *LoggerOptions) Logger {
var debugLogger *log.Logger
if options.Debug {
debugLogger = log.New(os.Stdout, "DEBUG ", log.LstdFlags)
}
return &DefaultLogger{
info: log.New(os.Stdout, "INFO ", log.LstdFlags),
warn: log.New(os.Stdout, "WARNING ", log.LstdFlags),
err: log.New(os.Stderr, "ERROR ", log.LstdFlags),
debug: debugLogger,
groups: []string{},
masks: []string{},
outputsFile: options.OutputsFile,
summaryFile: options.SummaryFile,
}
}
// formatMessage wraps the message with the current group and removes any masked values.
func (l *DefaultLogger) formatMessage(message string) string {
message = l.removeMasks(message)
message = l.addGroups(message)
return message
}
// removeMasks removes any masked values from the message.
func (l *DefaultLogger) removeMasks(message string) string {
for _, mask := range l.masks {
message = strings.ReplaceAll(message, mask, "********")
}
return message
}
// addGroups adds the current group to the message.
func (l *DefaultLogger) addGroups(message string) string {
for _, group := range l.groups {
message = fmt.Sprint(group, " - ", message)
}
return message
}
// Infof logs an info message.
func (l *DefaultLogger) Infof(format string, args ...interface{}) {
l.info.Printf(l.formatMessage(format), args...)
}
// Warnf logs a warning message.
func (l *DefaultLogger) Warnf(format string, args ...interface{}) {
l.warn.Printf(l.formatMessage(format), args...)
}
// Errorf logs an error message.
func (l *DefaultLogger) Errorf(format string, args ...interface{}) {
l.err.Printf(l.formatMessage(format), args...)
}
// Debugf logs a debug message if debug logging is enabled.
func (l *DefaultLogger) Debugf(format string, args ...interface{}) {
if l.debug != nil {
l.debug.Printf(l.formatMessage(format), args...)
}
}
// NoticeMessage sends a notice about a line.
func (l *DefaultLogger) NoticeMessage(message string, input NoticeMessageInput) {
var prefix string = ""
if input.Title != nil {
prefix += " title=" + *input.Title
}
if input.Filename != nil {
prefix += " file=" + *input.Filename
}
if input.Line != nil {
prefix += " line=" + fmt.Sprint(*input.Line)
}
if input.EndLine != nil {
prefix += " endLine=" + fmt.Sprint(*input.EndLine)
}
if input.Col != nil {
prefix += " col=" + fmt.Sprint(*input.Col)
}
if input.EndCol != nil {
prefix += " endColumn=" + fmt.Sprint(*input.EndCol)
}
l.info.Printf(l.formatMessage(fmt.Sprintf("%s %s", prefix, message)))
}
// Mask hides a value in the log output.
func (l *DefaultLogger) Mask(value string) {
l.masks = append(l.masks, value)
}
// StartGroup groups together log messages.
func (l *DefaultLogger) StartGroup(name string) {
l.groups = append(l.groups, name)
}
// EndGroup ends a group.
func (l *DefaultLogger) EndGroup() {
if len(l.groups) > 0 {
l.groups = l.groups[:len(l.groups)-1]
}
}
// SetOutput outputs a key-value pair to the outputs file.
func (l *DefaultLogger) SetOutput(name string, value string) {
if l.outputsFile == "" {
return
}
f, err := os.OpenFile(l.outputsFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
l.Errorf("failed to open outputs file: %s", err)
return
}
defer f.Close()
if _, err := f.WriteString(fmt.Sprintf("%s=%s", name, value)); err != nil {
l.Errorf("failed to write to outputs file: %s", err)
return
}
}
// SetSummary outputs a markdown-formatted summary to the summary file.
func (l *DefaultLogger) SetSummary(summary string) {
if l.summaryFile == "" {
return
}
f, err := os.OpenFile(l.summaryFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
l.Errorf("failed to open summary file: %s", err)
return
}
defer f.Close()
if _, err := f.WriteString(summary); err != nil {
l.Errorf("failed to write to summary file: %s", err)
return
}
}

51
main.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"os"
"runtime"
"github.com/yeslayla/godot-build-tools/internal"
"github.com/yeslayla/godot-build-tools/logging"
)
func main() {
logger := logging.NewLogger(&logging.LoggerOptions{})
var targetOS internal.TargetOS
switch runtime.GOOS {
case "linux":
targetOS = internal.TargetOSLinux
case "windows":
targetOS = internal.TargetOSWindows
case "darwin":
targetOS = internal.TargetOSMacOS
}
GodotSetup(logger, targetOS, "3.3.2", "stable")
}
func GodotSetup(logger logging.Logger, targetOS internal.TargetOS, version string, release string) (string, bool) {
logger.StartGroup("Godot Setup")
defer logger.EndGroup()
downloader := internal.NewDownloader(internal.TargetOSLinux, logger, &internal.DownloaderOptions{})
logger.Infof("Downloading Godot")
godotPackage, err := downloader.DownloadGodot(internal.TargetOSLinux, version, release)
if err != nil {
logger.Errorf("Failed to download Godot: %s", err)
return "", false
}
defer os.Remove(godotPackage)
logger.Infof("Godot package: %s", godotPackage)
logger.Infof("Installing Godot")
godotBin, err := downloader.InstallGodot(godotPackage, internal.TargetOSLinux, version, release)
if err != nil {
logger.Errorf("Failed to install Godot: %s", err)
return "", false
}
logger.Infof("Godot binary: %s", godotBin)
return godotBin, true
}

64
utils/unzip.go Normal file
View File

@ -0,0 +1,64 @@
package utils
import (
"archive/zip"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
)
// Unzip unzips a zip archive and returns the paths of the unzipped files.
func Unzip(archivePath string) ([]string, error) {
reader, err := zip.OpenReader(archivePath)
if err != nil {
return nil, fmt.Errorf("failed to open Godot package: %s", err)
}
defer reader.Close()
destDir, err := filepath.Abs(path.Dir(archivePath))
if err != nil {
return nil, fmt.Errorf("failed to get absolute path of Godot package: %s", err)
}
var unzippedFiles []string = make([]string, 0)
for _, file := range reader.File {
filePath := filepath.Join(destDir, file.Name)
if !strings.HasPrefix(filePath, filepath.Clean(destDir)+string(os.PathSeparator)) {
return unzippedFiles, fmt.Errorf("%s: illegal file path", filePath)
}
if file.FileInfo().IsDir() {
if err := os.MkdirAll(filePath, os.ModePerm); err != nil {
return unzippedFiles, fmt.Errorf("failed to create directory: %s", err)
}
continue
}
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
return unzippedFiles, fmt.Errorf("failed to create directory: %s", err)
}
destFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return unzippedFiles, fmt.Errorf("failed to open file: %s", err)
}
defer destFile.Close()
zippedFile, err := file.Open()
if err != nil {
return unzippedFiles, fmt.Errorf("failed to open zipped file: %s", err)
}
defer zippedFile.Close()
if _, err := io.Copy(destFile, zippedFile); err != nil {
return unzippedFiles, fmt.Errorf("failed to copy file: %s", err)
}
unzippedFiles = append(unzippedFiles, filePath)
}
return unzippedFiles, nil
}