Skip to content

Commit

Permalink
build: refactor version info and icon resource handling on windows
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
albertony committed Nov 1, 2023
1 parent 4ab57eb commit 4506f35
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 116 deletions.
1 change: 0 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ Thumbs.db
__pycache__
.DS_Store
/docs/static/img/logos/
resource_windows_*.syso
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`
Expand Down Expand Up @@ -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 ***"
Expand Down
148 changes: 37 additions & 111 deletions bin/cross-compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"log"
Expand All @@ -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")
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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, "-")
Expand All @@ -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 {
Expand All @@ -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, ",")...)
Expand Down
122 changes: 122 additions & 0 deletions bin/resource_windows.go
Original file line number Diff line number Diff line change
@@ -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_<arch>.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)
}
}
Loading

0 comments on commit 4506f35

Please sign in to comment.