diff --git a/.gitignore b/.gitignore index 2cf9683..5b95c39 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ dist/ /.venv /redbot-update *.exe +**/resources/generated/ +resources_windows.syso diff --git a/Makefile b/Makefile index 2780cb0..fa272d3 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ build-all: build-go build-python .PHONY: build-go build-go: + CGO_ENABLED=1 go generate ./... CGO_ENABLED=1 go build ./... CGO_ENABLED=1 go build ./go/cmd/redbot-update @@ -38,6 +39,7 @@ fmt format reformat: fmt-go fmt-python .PHONY: fmt-go format-go reformat-go fmt-go format-go reformat-go: go fmt ./... + cd ./go/build_tools && go fmt ./... .PHONY: fmt-python format-python reformat-python fmt-python format-python reformat-python: diff --git a/go.mod b/go.mod index 2945747..24d5221 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,10 @@ module github.com/cog-creators/redbot-update-wrapper go 1.26 toolchain go1.26.3 + +tool github.com/josephspurrier/goversioninfo/cmd/goversioninfo + +require ( + github.com/akavel/rsrc v0.10.2 // indirect + github.com/josephspurrier/goversioninfo v1.7.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9bff07c --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/josephspurrier/goversioninfo v1.7.0 h1:LQzXOlVm/CtbwJ9/UHl5a2HT0BjcLAwid5gqGd7ZUJ8= +github.com/josephspurrier/goversioninfo v1.7.0/go.mod h1:z9y0r2G6g5jwSJaFE0cxW9to0aeIibK7UYeLx53aQRU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work new file mode 100644 index 0000000..bd563dd --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.26.3 + +use ( + . + ./go/build_tools +) diff --git a/go/build_tools/generate_versioninfo/main.go b/go/build_tools/generate_versioninfo/main.go new file mode 100644 index 0000000..8f40e53 --- /dev/null +++ b/go/build_tools/generate_versioninfo/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "text/template" + + "github.com/BurntSushi/toml" +) + +const ( + resourcesDir = "resources" + baseResourcesDir = resourcesDir + string(filepath.Separator) + "base" + generatedResourcesDir = resourcesDir + string(filepath.Separator) + "generated" +) + +type Project struct { + Version string `toml:"version"` +} + +type PyProject struct { + Project Project `toml:"project"` +} + +func main() { + projectDir := os.Args[1] + projectFile := filepath.Join(projectDir, "pyproject.toml") + + var pyproject PyProject + if _, err := toml.DecodeFile(projectFile, &pyproject); err != nil { + panic(err) + } + rawVersion := pyproject.Project.Version + v := parsePythonProjectVersion(rawVersion) + + if err := os.MkdirAll(generatedResourcesDir, 0700); err != nil { + panic(err) + } + generateAppManifestFile(v) + generateVersionInfoFile(v) +} + +// generate app manifest +func generateAppManifestFile(v PythonProjectVersion) { + t, err := template.ParseFiles(filepath.Join(baseResourcesDir, "app.manifest.tmpl")) + if err != nil { + panic(err) + } + f, err := os.Create(filepath.Join(generatedResourcesDir, "app.manifest")) + if err != nil { + panic(err) + } + defer f.Close() + t.Execute(f, struct{ ProductVersion string }{v.FileVersion().String()}) + if err := f.Close(); err != nil { + panic(err) + } +} + +// generate versioninfo.json +func generateVersionInfoFile(v PythonProjectVersion) { + data, err := os.ReadFile(filepath.Join(baseResourcesDir, "versioninfo.json")) + if err != nil { + panic(err) + } + versioninfo := map[string]any{} + json.Unmarshal(data, &versioninfo) + + fixedFileInfo := versioninfo["FixedFileInfo"].(map[string]any) + fixedFileInfo["FileVersion"] = v.FileVersion() + fixedFileInfo["ProductVersion"] = v.FileVersion() + stringFileInfo := versioninfo["StringFileInfo"].(map[string]any) + stringFileInfo["FileVersion"] = v.String() + stringFileInfo["ProductVersion"] = v.String() + + data, err = json.Marshal(versioninfo) + if err != nil { + panic(err) + } + err = os.WriteFile(filepath.Join(generatedResourcesDir, "versioninfo.json"), data, 0600) + if err != nil { + panic(err) + } +} diff --git a/go/build_tools/generate_versioninfo/utils.go b/go/build_tools/generate_versioninfo/utils.go new file mode 100644 index 0000000..7f9e2dc --- /dev/null +++ b/go/build_tools/generate_versioninfo/utils.go @@ -0,0 +1,15 @@ +package main + +import "strconv" + +type Int interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 +} + +func ParseInt[T Int](s string) T { + i, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return T(i) +} diff --git a/go/build_tools/generate_versioninfo/version.go b/go/build_tools/generate_versioninfo/version.go new file mode 100644 index 0000000..7d2f332 --- /dev/null +++ b/go/build_tools/generate_versioninfo/version.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "regexp" +) + +type ReleaseLevel string + +var ( + ReleaseLevelAlpha ReleaseLevel = "a" + ReleaseLevelBeta ReleaseLevel = "b" + ReleaseLevelReleaseCandidate ReleaseLevel = "rc" + ReleaseLevelFinal ReleaseLevel = "" + + pythonProjectVersionRegexp = regexp.MustCompile( + `(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:(a|b|rc)(0|[1-9]\d*))?`, + ) + pythonSerialToFileVersionMap = map[ReleaseLevel]uint16{ + ReleaseLevelAlpha: 0x2000, + ReleaseLevelBeta: 0x4000, + ReleaseLevelReleaseCandidate: 0x6000, + ReleaseLevelFinal: 0x8000, + } +) + +type FileVersion struct { + Major uint16 + Minor uint16 + Patch uint16 + Build uint16 +} + +func (fv FileVersion) String() string { + return fmt.Sprintf("%v.%v.%v.%v", fv.Major, fv.Minor, fv.Patch, fv.Build) +} + +type PythonProjectVersion struct { + Major uint16 + Minor uint16 + Micro uint16 + ReleaseLevel ReleaseLevel + Serial uint8 +} + +func parsePythonProjectVersion(v string) PythonProjectVersion { + matches := pythonProjectVersionRegexp.FindStringSubmatch(v) + if matches == nil { + panic("could not parse Python project version") + } + ppv := PythonProjectVersion{} + ppv.Major = ParseInt[uint16](matches[1]) + ppv.Minor = ParseInt[uint16](matches[2]) + ppv.Micro = ParseInt[uint16](matches[3]) + ppv.ReleaseLevel = ReleaseLevel(matches[4]) + if matches[5] != "" { + ppv.Serial = ParseInt[uint8](matches[5]) + } + return ppv +} + +func (ppv PythonProjectVersion) String() string { + baseVersion := fmt.Sprintf("%v.%v.%v", ppv.Major, ppv.Minor, ppv.Micro) + if ppv.ReleaseLevel == ReleaseLevelFinal { + return baseVersion + } + return fmt.Sprintf("%v%v%v", baseVersion, ppv.ReleaseLevel, ppv.Serial) +} + +func (ppv PythonProjectVersion) FileVersion() FileVersion { + return FileVersion{ + Major: ppv.Major, + Minor: ppv.Minor, + Patch: ppv.Micro, + Build: pythonSerialToFileVersionMap[ppv.ReleaseLevel] | uint16(ppv.Serial), + } +} diff --git a/go/build_tools/go.mod b/go/build_tools/go.mod new file mode 100644 index 0000000..283f7a2 --- /dev/null +++ b/go/build_tools/go.mod @@ -0,0 +1,5 @@ +module github.com/cog-creators/redbot-update-wrapper/go/build_tools + +go 1.26.3 + +require github.com/BurntSushi/toml v1.6.0 diff --git a/go/build_tools/go.sum b/go/build_tools/go.sum new file mode 100644 index 0000000..f74b269 --- /dev/null +++ b/go/build_tools/go.sum @@ -0,0 +1,2 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= diff --git a/go/cmd/redbot-update/main.go b/go/cmd/redbot-update/main.go index fee840f..5614102 100644 --- a/go/cmd/redbot-update/main.go +++ b/go/cmd/redbot-update/main.go @@ -1,3 +1,6 @@ +//go:generate go run ../../build_tools/generate_versioninfo ../../.. +//go:generate go tool github.com/josephspurrier/goversioninfo/cmd/goversioninfo -o resources_windows.syso resources/generated/versioninfo.json + package main import ( diff --git a/go/cmd/redbot-update/resources/base/app.manifest.tmpl b/go/cmd/redbot-update/resources/base/app.manifest.tmpl new file mode 100644 index 0000000..4a89674 --- /dev/null +++ b/go/cmd/redbot-update/resources/base/app.manifest.tmpl @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/go/cmd/redbot-update/resources/base/versioninfo.json b/go/cmd/redbot-update/resources/base/versioninfo.json new file mode 100644 index 0000000..b4fdaeb --- /dev/null +++ b/go/cmd/redbot-update/resources/base/versioninfo.json @@ -0,0 +1,23 @@ +{ + "FixedFileInfo": { + "FileFlagsMask": "3f", + "FileFlags ": "00", + "FileOS": "040004", + "FileType": "01", + "FileSubType": "00" + }, + "StringFileInfo": { + "Comments": "", + "CompanyName": "Cog Creators", + "FileDescription": "redbot-update wrapper", + "InternalName": "redbot-update.exe", + "LegalCopyright": "Copyright (c) 2026 Cog Creators (https://github.com/Cog-Creators)", + "LegalTrademarks": "", + "OriginalFilename": "redbot-update.exe", + "PrivateBuild": "", + "ProductName": "redbot-update wrapper", + "SpecialBuild": "" + }, + "IconPath": "resources/cog-creators.ico", + "ManifestPath": "resources/generated/app.manifest" +} diff --git a/go/cmd/redbot-update/resources/cog-creators.ico b/go/cmd/redbot-update/resources/cog-creators.ico new file mode 100644 index 0000000..47bb61c Binary files /dev/null and b/go/cmd/redbot-update/resources/cog-creators.ico differ diff --git a/hatch_build.py b/hatch_build.py index b6f268d..4635b44 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -198,6 +198,12 @@ def dependencies(self) -> List[str]: def initialize(self, version: str, build_data: Dict[str, Any]) -> None: if self.target_name == "sdist": + go_bin_args = _get_go_bin_args(prefer_system=not self._force_use_go_bin) + generate_command = (*go_bin_args, "generate", "./go/cmd/redbot-update") + + self.app.display_info("Generating redbot-update resources") + subprocess.check_call(generate_command) + self.app.display_success("Generated redbot-update resources") return if version == "editable":