From 4506f35f2e00ef4ed8075809e622abbbcc414ad6 Mon Sep 17 00:00:00 2001 From: albertony <12441419+albertony@users.noreply.github.com> Date: Wed, 28 Sep 2022 14:37:38 +0200 Subject: [PATCH] build: refactor version info and icon resource handling on windows This makes it easier to add resources with any build method, and also when building librclone.dll. Goversioninfo is now used as a library, instead of running it as a tool. --- .github/workflows/build.yml | 1 - .gitignore | 1 + Makefile | 11 ++- bin/cross-compile.go | 148 +++++++++--------------------------- bin/resource_windows.go | 122 +++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 5 ++ 7 files changed, 174 insertions(+), 116 deletions(-) create mode 100644 bin/resource_windows.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f049ab7be..d12696daf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -216,7 +216,6 @@ jobs: shell: bash run: | if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then make release_dep_linux ; fi - if [[ "${{ matrix.os }}" == "windows-latest" ]]; then make release_dep_windows ; fi make ci_beta env: RCLONE_CONFIG_PASS: ${{ secrets.RCLONE_CONFIG_PASS }} diff --git a/.gitignore b/.gitignore index c65b48913..f7cd88d21 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ Thumbs.db __pycache__ .DS_Store /docs/static/img/logos/ +resource_windows_*.syso diff --git a/Makefile b/Makefile index 434c3f4a2..3579cf24c 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ ifdef RELEASE_TAG TAG := $(RELEASE_TAG) endif GO_VERSION := $(shell go version) +GO_OS := $(shell go env GOOS) ifdef BETA_SUBDIR BETA_SUBDIR := /$(BETA_SUBDIR) endif @@ -46,7 +47,13 @@ endif .PHONY: rclone test_all vars version rclone: +ifeq ($(GO_OS),windows) + go run bin/resource_windows.go -version $(TAG) -syso resource_windows_`go env GOARCH`.syso +endif go build -v --ldflags "-s -X github.com/rclone/rclone/fs.Version=$(TAG)" $(BUILDTAGS) $(BUILD_ARGS) +ifeq ($(GO_OS),windows) + rm resource_windows_`go env GOARCH`.syso +endif mkdir -p `go env GOPATH`/bin/ cp -av rclone`go env GOEXE` `go env GOPATH`/bin/rclone`go env GOEXE`.new mv -v `go env GOPATH`/bin/rclone`go env GOEXE`.new `go env GOPATH`/bin/rclone`go env GOEXE` @@ -102,10 +109,6 @@ build_dep: release_dep_linux: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest -# Get the release dependencies we only install on Windows -release_dep_windows: - GOOS="" GOARCH="" go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest - # Update dependencies showupdates: @echo "*** Direct dependencies that could be updated ***" diff --git a/bin/cross-compile.go b/bin/cross-compile.go index 82be6015e..f0a02336f 100644 --- a/bin/cross-compile.go +++ b/bin/cross-compile.go @@ -6,7 +6,6 @@ package main import ( - "encoding/json" "flag" "fmt" "log" @@ -21,23 +20,21 @@ import ( "sync" "text/template" "time" - - "github.com/coreos/go-semver/semver" ) var ( // Flags - debug = flag.Bool("d", false, "Print commands instead of running them.") - parallel = flag.Int("parallel", runtime.NumCPU(), "Number of commands to run in parallel.") + debug = flag.Bool("d", false, "Print commands instead of running them") + parallel = flag.Int("parallel", runtime.NumCPU(), "Number of commands to run in parallel") copyAs = flag.String("release", "", "Make copies of the releases with this name") gitLog = flag.String("git-log", "", "git log to include as well") include = flag.String("include", "^.*$", "os/arch regexp to include") exclude = flag.String("exclude", "^$", "os/arch regexp to exclude") cgo = flag.Bool("cgo", false, "Use cgo for the build") - noClean = flag.Bool("no-clean", false, "Don't clean the build directory before running.") + noClean = flag.Bool("no-clean", false, "Don't clean the build directory before running") tags = flag.String("tags", "", "Space separated list of build tags") buildmode = flag.String("buildmode", "", "Passed to go build -buildmode flag") - compileOnly = flag.Bool("compile-only", false, "Just build the binary, not the zip.") + compileOnly = flag.Bool("compile-only", false, "Just build the binary, not the zip") extraEnv = flag.String("env", "", "comma separated list of VAR=VALUE env vars to set") macOSSDK = flag.String("macos-sdk", "", "macOS SDK to use") macOSArch = flag.String("macos-arch", "", "macOS arch to use") @@ -140,21 +137,21 @@ func chdir(dir string) { func substitute(inFile, outFile string, data interface{}) { t, err := template.ParseFiles(inFile) if err != nil { - log.Fatalf("Failed to read template file %q: %v %v", inFile, err) + log.Fatalf("Failed to read template file %q: %v", inFile, err) } out, err := os.Create(outFile) if err != nil { - log.Fatalf("Failed to create output file %q: %v %v", outFile, err) + log.Fatalf("Failed to create output file %q: %v", outFile, err) } defer func() { err := out.Close() if err != nil { - log.Fatalf("Failed to close output file %q: %v %v", outFile, err) + log.Fatalf("Failed to close output file %q: %v", outFile, err) } }() err = t.Execute(out, data) if err != nil { - log.Fatalf("Failed to substitute template file %q: %v %v", inFile, err) + log.Fatalf("Failed to substitute template file %q: %v", inFile, err) } } @@ -202,101 +199,6 @@ func buildDebAndRpm(dir, version, goarch string) []string { return artifacts } -// generate system object (syso) file to be picked up by a following go build for embedding icon and version info resources into windows executable -func buildWindowsResourceSyso(goarch string, versionTag string) string { - type M map[string]interface{} - version := strings.TrimPrefix(versionTag, "v") - semanticVersion := semver.New(version) - - // Build json input to goversioninfo utility - bs, err := json.Marshal(M{ - "FixedFileInfo": M{ - "FileVersion": M{ - "Major": semanticVersion.Major, - "Minor": semanticVersion.Minor, - "Patch": semanticVersion.Patch, - }, - "ProductVersion": M{ - "Major": semanticVersion.Major, - "Minor": semanticVersion.Minor, - "Patch": semanticVersion.Patch, - }, - }, - "StringFileInfo": M{ - "CompanyName": "https://rclone.org", - "ProductName": "Rclone", - "FileDescription": "Rclone", - "InternalName": "rclone", - "OriginalFilename": "rclone.exe", - "LegalCopyright": "The Rclone Authors", - "FileVersion": version, - "ProductVersion": version, - }, - "IconPath": "../graphics/logo/ico/logo_symbol_color.ico", - }) - if err != nil { - log.Printf("Failed to build version info json: %v", err) - return "" - } - - // Write json to temporary file that will only be used by the goversioninfo command executed below. - jsonPath, err := filepath.Abs("versioninfo_windows_" + goarch + ".json") // Appending goos and goarch as suffix to avoid any race conditions - if err != nil { - log.Printf("Failed to resolve path: %v", err) - return "" - } - err = os.WriteFile(jsonPath, bs, 0644) - if err != nil { - log.Printf("Failed to write %s: %v", jsonPath, err) - return "" - } - defer func() { - if err := os.Remove(jsonPath); err != nil { - if !os.IsNotExist(err) { - log.Printf("Warning: Couldn't remove generated %s: %v. Please remove it manually.", jsonPath, err) - } - } - }() - - // Execute goversioninfo utility using the json file as input. - // It will produce a system object (syso) file that a following go build should pick up. - sysoPath, err := filepath.Abs("../resource_windows_" + goarch + ".syso") // Appending goos and goarch as suffix to avoid any race conditions, and also it is recognized by go build and avoids any builds for other systems considering it - if err != nil { - log.Printf("Failed to resolve path: %v", err) - return "" - } - args := []string{ - "goversioninfo", - "-o", - sysoPath, - } - if strings.Contains(goarch, "64") { - args = append(args, "-64") // Make the syso a 64-bit coff file - } - if strings.Contains(goarch, "arm") { - args = append(args, "-arm") // Make the syso an arm binary - } - args = append(args, jsonPath) - err = runEnv(args, nil) - if err != nil { - return "" - } - - return sysoPath -} - -// delete generated system object (syso) resource file -func cleanupResourceSyso(sysoFilePath string) { - if sysoFilePath == "" { - return - } - if err := os.Remove(sysoFilePath); err != nil { - if !os.IsNotExist(err) { - log.Printf("Warning: Couldn't remove generated %s: %v. Please remove it manually.", sysoFilePath, err) - } - } -} - // Trip a version suffix off the arch if present func stripVersion(goarch string) string { i := strings.Index(goarch, "-") @@ -315,17 +217,41 @@ func runOut(command ...string) string { return strings.TrimSpace(string(out)) } +// Generate Windows resource system object file (.syso), which can be picked +// up by the following go build for embedding version information and icon +// resources into the executable. +func generateResourceWindows(version, arch string) func() { + sysoPath := fmt.Sprintf("../resource_windows_%s.syso", arch) // Use explicit destination filename, even though it should be same as default, so that we are sure we have the correct reference to it + if err := os.Remove(sysoPath); !os.IsNotExist(err) { + // Note: This one we choose to treat as fatal, to avoid any risk of picking up an old .syso file without noticing. + log.Fatalf("Failed to remove existing Windows %s resource system object file %s: %v", arch, sysoPath, err) + } + args := []string{"go", "run", "../bin/resource_windows.go", "-arch", arch, "-version", version, "-syso", sysoPath} + if err := runEnv(args, nil); err != nil { + log.Printf("Warning: Couldn't generate Windows %s resource system object file, binaries will not have version information or icon embedded", arch) + return nil + } + if _, err := os.Stat(sysoPath); err != nil { + log.Printf("Warning: Couldn't find generated Windows %s resource system object file, binaries will not have version information or icon embedded", arch) + return nil + } + return func() { + if err := os.Remove(sysoPath); err != nil && !os.IsNotExist(err) { + log.Printf("Warning: Couldn't remove generated Windows %s resource system object file %s: %v. Please remove it manually.", arch, sysoPath, err) + } + } +} + // build the binary in dir returning success or failure func compileArch(version, goos, goarch, dir string) bool { log.Printf("Compiling %s/%s into %s", goos, goarch, dir) + goarchBase := stripVersion(goarch) output := filepath.Join(dir, "rclone") if goos == "windows" { output += ".exe" - sysoPath := buildWindowsResourceSyso(goarch, version) - if sysoPath == "" { - log.Printf("Warning: Windows binaries will not have file information embedded") + if cleanupFn := generateResourceWindows(version, goarchBase); cleanupFn != nil { + defer cleanupFn() } - defer cleanupResourceSyso(sysoPath) } err := os.MkdirAll(dir, 0777) if err != nil { @@ -348,7 +274,7 @@ func compileArch(version, goos, goarch, dir string) bool { ) env := []string{ "GOOS=" + goos, - "GOARCH=" + stripVersion(goarch), + "GOARCH=" + goarchBase, } if *extraEnv != "" { env = append(env, strings.Split(*extraEnv, ",")...) diff --git a/bin/resource_windows.go b/bin/resource_windows.go new file mode 100644 index 000000000..2f74ad363 --- /dev/null +++ b/bin/resource_windows.go @@ -0,0 +1,122 @@ +// Utility program to generate Rclone-specific Windows resource system object +// file (.syso), that can be picked up by a following go build for embedding +// version information and icon resources into a rclone binary. +// +// Run it with "go generate", or "go run" to be able to customize with +// command-line flags. Note that this program is intended to be run directly +// from its original location in the source tree: Default paths are absolute +// within the current source tree, which is convenient because it makes it +// oblivious to the working directory, and it gives identical result whether +// run by "go generate" or "go run", but it will not make sense if this +// program's source is moved out from the source tree. +// +// Can be used for rclone.exe (default), and other binaries such as +// librclone.dll (must be specified with flag -binary). +// + +//go:generate go run resource_windows.go +//go:build tools +// +build tools + +package main + +import ( + "flag" + "fmt" + "log" + "path" + "runtime" + "strings" + + "github.com/coreos/go-semver/semver" + "github.com/josephspurrier/goversioninfo" + "github.com/rclone/rclone/fs" +) + +func main() { + // Get path of directory containing the current source file to use for absolute path references within the code tree (as described above) + projectDir := "" + _, sourceFile, _, ok := runtime.Caller(0) + if ok { + projectDir = path.Dir(path.Dir(sourceFile)) // Root of the current project working directory + } + + // Define flags + binary := flag.String("binary", "rclone.exe", `The name of the binary to generate resource for, e.g. "rclone.exe" or "librclone.dll"`) + arch := flag.String("arch", runtime.GOARCH, `Architecture of resource file, or the target GOARCH, "386", "amd64", "arm", or "arm64"`) + version := flag.String("version", fs.Version, "Version number or tag name") + icon := flag.String("icon", path.Join(projectDir, "graphics/logo/ico/logo_symbol_color.ico"), "Path to icon file to embed in an .exe binary") + dir := flag.String("dir", projectDir, "Path to output directory where to write the resulting system object file (.syso), with a default name according to -arch (resource_windows_.syso), only considered if not -syso is specified") + syso := flag.String("syso", "", "Path to output resource system object file (.syso) to be created/overwritten, ignores -dir") + + // Parse command-line flags + flag.Parse() + + // Handle default value for -file which depends on optional -dir and -arch + if *syso == "" { + // Use default filename, which includes target GOOS (hardcoded "windows") + // and GOARCH (from argument -arch) as suffix, to avoid any race conditions, + // and also this will be recognized by go build when it is consuming the + // .syso file and will only be used for builds with matching os/arch. + *syso = path.Join(*dir, fmt.Sprintf("resource_windows_%s.syso", *arch)) + } + + // Parse version/tag string argument as a SemVer + stringVersion := strings.TrimPrefix(*version, "v") + semanticVersion, err := semver.NewVersion(stringVersion) + if err != nil { + log.Fatalf("Invalid version number: %v", err) + } + + // Extract binary extension + binaryExt := path.Ext(*binary) + + // Create the version info configuration container + vi := &goversioninfo.VersionInfo{} + + // FixedFileInfo + vi.FixedFileInfo.FileOS = "040004" // VOS_NT_WINDOWS32 + if strings.EqualFold(binaryExt, ".exe") { + vi.FixedFileInfo.FileType = "01" // VFT_APP + } else if strings.EqualFold(binaryExt, ".dll") { + vi.FixedFileInfo.FileType = "02" // VFT_DLL + } else { + log.Fatalf("Specified binary must have extension .exe or .dll") + } + // FixedFileInfo.FileVersion + vi.FixedFileInfo.FileVersion.Major = int(semanticVersion.Major) + vi.FixedFileInfo.FileVersion.Minor = int(semanticVersion.Minor) + vi.FixedFileInfo.FileVersion.Patch = int(semanticVersion.Patch) + vi.FixedFileInfo.FileVersion.Build = 0 + // FixedFileInfo.ProductVersion + vi.FixedFileInfo.ProductVersion.Major = int(semanticVersion.Major) + vi.FixedFileInfo.ProductVersion.Minor = int(semanticVersion.Minor) + vi.FixedFileInfo.ProductVersion.Patch = int(semanticVersion.Patch) + vi.FixedFileInfo.ProductVersion.Build = 0 + + // StringFileInfo + vi.StringFileInfo.CompanyName = "https://rclone.org" + vi.StringFileInfo.ProductName = "Rclone" + vi.StringFileInfo.FileDescription = "Rclone" + vi.StringFileInfo.InternalName = (*binary)[:len(*binary)-len(binaryExt)] + vi.StringFileInfo.OriginalFilename = *binary + vi.StringFileInfo.LegalCopyright = "The Rclone Authors" + vi.StringFileInfo.FileVersion = stringVersion + vi.StringFileInfo.ProductVersion = stringVersion + + // Icon (only relevant for .exe, not .dll) + if *icon != "" && strings.EqualFold(binaryExt, ".exe") { + vi.IconPath = *icon + } + + // Build native structures from the configuration data + vi.Build() + + // Write the native structures as binary data to a buffer + vi.Walk() + + // Write the binary data buffer to file + if err := vi.WriteSyso(*syso, *arch); err != nil { + log.Fatalf(`Failed to generate Windows %s resource system object file for %v with path "%v": %v`, *arch, *binary, *syso, err) + } +} diff --git a/go.mod b/go.mod index 5230d9cc2..465758f5e 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/iguanesolutions/go-systemd/v5 v5.1.1 github.com/jcmturner/gokrb5/v8 v8.4.4 github.com/jlaffaye/ftp v0.2.0 + github.com/josephspurrier/goversioninfo v1.4.0 github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 github.com/klauspost/compress v1.17.2 github.com/koofr/go-httpclient v0.0.0-20230225102643-5d51a2e9dea6 @@ -90,6 +91,7 @@ require ( github.com/ProtonMail/go-srp v0.0.7 // indirect github.com/ProtonMail/gopenpgp/v2 v2.7.4 // indirect github.com/PuerkitoBio/goquery v1.8.1 // indirect + github.com/akavel/rsrc v0.10.2 // indirect github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index 276dccb22..98e7b41a2 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,8 @@ github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3 h1:hhdWprfSpFbN7lz3W github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3/go.mod h1:XaUnRxSCYgL3kkgX0QHIV0D+znljPIDImxlv2kbGv0Y= github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= +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/anacrolix/dms v1.6.0 h1:v2g1Y+Fc/ICSEc+7M6B92oFcfcqa5LXYPhE4Hcm5tVA= github.com/anacrolix/dms v1.6.0/go.mod h1:5fAMpBcPFG4WQFh91zhf2E7/KYZ3/WmmRAf/WMoL0Q0= github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= @@ -329,6 +331,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= +github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -488,6 +492,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=