8000 Embed Windows resources (VERSIONINFO) during build by babakks · Pull Request #11048 · cli/cli · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Embed Windows resources (VERSIONINFO) during build #11048

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 24, 2025

Conversation

babakks
Copy link
Member
@babakks babakks commented May 30, 2025

Fixes #10601

This PR adds another Before hook to our Goreleaser config to prepare Windows resources information (version info) as .syso files to be embedded into our .exe binary during build.

There are a couple of notes regarding this implementation:

  • A new file, named script/winres.json is added to hold the static metadata (e.g. company, product name, etc). The version fields are left empty in this file, which will be later populated during the call to the newly added script/gen-winres.ps1 script. I didn't include the icon, though.

  • The script/gen-winres.ps1 is PowerShell script that creates .syso files and moves them to cmd/gh to be picked up by Go compiler. Leaving them at the root of the repo has no effect. Under the hood the script uses the github.com/gc-hib/go-winres@v0.3.3 module's make command, to creates multiple .syso files per architecture. Thanks to the right naming (e.g. rsrc_windows_amd64.syso), the Go compiler only picks the one that matches the target architecture. So, we can simply create all .syso files at the beginning of the goreleaser session and leave the rest to Go compiler. Not to mention, leaving the files will have no effect when building for other platforms; although, we actually build them as separete jobs.

  • The FileVersion field of the Windows resources structure is meant to be a 4-component version (e.g. 1.1.1.1). I tried to assign it with our 3-component semver version but a zero 4th component appears in the GUI (See below). So, I think it's best to leave the FileVersion empty (which will be displayed as 0.0.0.0) and just populate the ProductVersion field, which is fine. I've explained this in the script. Please let me know if you think it's worth having the FileVersion set.

    file-version-issue

Verifying the binaries

To make sure the implementation is correct, I tried it with a simplified workflow that goes through the same steps as our deployment workflow, and then uploads the content of the dist directory as artifacts so you can download it and check the .exe files. The workflow file is included at the end. You can run it on your own forks. However, make sure you're building the tag created from the last commit, otherwise the latest changes to the .goreleaser.yml file will not be used. By the way, this is the same workflow, ran on my own fork, and it's how the created .exe looks:

metadata-embedded

For checking the .exe files, make sure you do it on Windows (through GUI or PowerShell). I don't know about macOS, but if you try PowerShell on Linux it will return an empty data structure (I learned this the hard way, 😄). Anyway, this is the PowerShell command to check the metadata being embedded:

[System.Diagnostics.FileVersionInfo]::GetVersionInfo("./gh.exe").ToString()

Sample workflow

ame: Test winres embedded resources
run-name: ${{ inputs.tag_name }} / ${{ inputs.environment }}

permissions:
  attestations: write
  contents: write
  id-token: write

on:
  workflow_dispatch:
    inputs:
      tag_name:
        description: 'Tag name to validate and use for the release'
        required: true
        type: string

jobs:
  validate-tag-name:
    runs-on: ubuntu-latest
    steps:
      - name: Validate tag name format
        run: |
          if [[ ! "${{ inputs.tag_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "Invalid tag name format. Must be in the form v1.2.3"
            exit 1
          fi

  windows:
    needs: validate-tag-name
    runs-on: windows-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version-file: 'go.mod'
      - name: Install GoReleaser
        uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552
        with:
          version: "~1.17.1"
          install-only: true
      - name: Build release binaries
        shell: bash
        env:
          TAG_NAME: ${{ inputs.tag_name }}
        run: script/release --local "$TAG_NAME" --platform windows
      - name: Upload binaries as artifacts
        uses: actions/upload-artifact@v4
        with:
          name: windows
          if-no-files-found: error
          retention-days: 7
          path: |
            dist/*.zip

babakks added 2 commits May 30, 2025 15:20
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
@Copilot Copilot AI review requested due to automatic review settings May 30, 2025 17:09
@babakks babakks requested a review from a team as a code owner May 30, 2025 17:09
@babakks babakks requested a review from williammartin May 30, 2025 17:09
@babakks babakks linked an issue May 30, 2025 that may be closed by this pull request
@babakks babakks temporarily deployed to cli-automation May 30, 2025 17:09 — with GitHub Actions Inactive
@babakks babakks requested review from andyfeller and BagToad May 30, 2025 17:10
Copy link
Contributor
@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for embedding Windows VERSIONINFO resources into the .exe build by generating .syso files from a static metadata file.

  • Introduces script/winres.json to hold placeholder metadata for Windows resources.
  • Adds script/gen-winres.ps1 to generate and move architecture-specific .syso files.
  • Updates .goreleaser.yml to invoke the PowerShell script before Windows builds.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
script/winres.json Defines structured placeholders for RT_GROUP_ICON, RT_MANIFEST, and RT_VERSION entries
script/gen-winres.ps1 PowerShell script that runs go-winres make and moves generated .syso files
.goreleaser.yml Added a before hook to run the generation script for embedding resources
Comments suppressed due to low confidence (2)

script/gen-winres.ps1:39

  • Use the correct variable casing in the error message ($_winresJson) to match the variable name and avoid confusion.
    Write-Host "error: winres.json file not found at '$_winresjson'"

script/gen-winres.ps1:54

  • Verify that the import path (github.com/tc-hib/go-winres) matches the intended module (the PR description mentions gc-hib). Update this path if it was mistyped.
go run github.com/tc-hib/go-winres@v0.3.3 make `

.goreleaser.yml Outdated
@@ -11,6 +11,8 @@ before:
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make manpages GH_VERSION={{.Version}}
- >- # On linux the completions are used in nfpms below, but on macos they are used outside in the deployment build.
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make completions
- >- # We need to create the `.syso` files (per architecture) to embed Windows resources (version info)
pwsh .\script\gen-winres.ps1 386,amd64,arm64 '{{ .Version }}' .\script\winres.json .\cmd\gh\
Copy link
Preview
Copilot AI May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap the new pwsh .\script\gen-winres.ps1 hook in a Windows-only conditional (e.g. {{ if eq .Runtime.Goos "windows" }}) to prevent it from running on non-Windows targets and failing the build.

Suggested change
pwsh .\script\gen-winres.ps1 386,amd64,arm64 '{{ .Version }}' .\script\winres.json .\cmd\gh\
{{ if eq .Runtime.Goos "windows" }}pwsh .\script\gen-winres.ps1 386,amd64,arm64 '{{ .Version }}' .\script\winres.json .\cmd\gh\{{ end }}

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .Runtime.Goos holds the platform where the Goreleaser is invoked. So, it's not the target.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@babakks : you're correct: GOOS in this case is the operating system of the runner, which isn't the target per se but it is because we Windows releases are built on the Windows runner.

That said, I think the before hooks above short circuit the make commands on the Windows runner, which would / should result in just echoing the commands without running them.

You can look back into #7324 where completions were only built for Windows and Mac OS:

cli/.goreleaser.yml

Lines 8 to 13 in 8b987e2

8000
before:
hooks:
- >-
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make manpages GH_VERSION={{.Version}}
- >-
{{ if ne .Runtime.Goos "linux" }}echo{{ end }} make completions

If these should only be present on Windows builds, then maybe this should be conditionalized for anything not Windows

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I noticed that, and it's annoying to see CI environment details are leaked into .goreleaser.yml. Anyway, I updated the command to only work on Windows.

babakks added 2 commits May 30, 2025 18:19
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
@caspChristian
Copy link
caspChristian commented May 30, 2025

Don't leave FileVersion as 0.0.0.0, both version fields are strings, but are in windows UI parsed to numeric only, even if the value is non numeric, 2.49.0.1, please ignore the UI.

Example for git.exe:

PS C:\> [System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\Program Files\Git\bin\git.exe").ProductVersion
2.49.0.windows.1
PS C:\> [System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\Program Files\Git\bin\git.exe").FileVersion
2.49.0.windows.1
PS C:\>

.NET binaries instead have data such as:

FileVersion: 13.0.2.27524
ProductVersion: 13.0.2+4fba53a324c445f06ee08e45a015c346000a7ef2
  • More or less anything goes, but don't leave FileVersion unset.
  • Consider including more details in ProductVersion (follow .net and include the +git commit syntax if available)

Edit: But in this case follow output of gh.exe --version and set it to the format of 2.72.0 (2025-04-30)

@babakks
Copy link
Member Author
babakks commented Jun 2, 2025

Thanks for the note, @caspChristian! 🙏 I can confirm that was just the GUI behaviour.

Regrading the ProductVersion field usage, I looked for a standard practice, but couldn't find any global practice. Both FileVersion and ProductVersion are used by other applications pretty much in a free-form kind of way. For example:

Application FileVersion ProductVersion
Git & Git-Bash 2.49.0.windows.1 2.49.0.windows.1
Windows Media Player 12.0.26100.1 (WinBuild.160101.0800) 12.0.26100.1
Microsoft Edge 135.0.3179.98 135.0.3179.98
Go and Gofmt (unset) (unset)

So, I think we should pick a format that works for us. As you pointed out, currently, the output of gh version looks like this:

$ gh version
gh version 2.74.0 (2025-05-29)
https://github.com/cli/cli/releases/tag/v2.74.0

The version and the build date are the only release-specific variables that we bake into the binary. So, I agree with your suggestion about using the gh version format. So, to recap here's the looks of FileVersion and ProductVersion:

  • FileVersion: 2.74.0 (2025-05-29)
  • ProductVersion: 2.74.0

@babakks
Copy link
Member Author
babakks commented Jun 2, 2025

@andyfeller Since you somewhat defined the A/C for this, can you please confirm the suggested format of FileVersion and ProductVersion? See my comment above.

Copy link
Member
@andyfeller andyfeller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: excellent PR write up!

Handful of questions and concerns I'd like to follow up on regarding automation and the underlying tool. 🤔

.goreleaser.yml Outdated
@@ -11,6 +11,8 @@ before:
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make manpages GH_VERSION={{.Version}}
- >- # On linux the completions are used in nfpms below, but on macos they are used outside in the deployment build.
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make completions
- >- # We need to create the `.syso` files (per architecture) to embed Windows resources (version info)
pwsh .\script\gen-winres.ps1 386,amd64,arm64 '{{ .Version }}' .\script\winres.json .\cmd\gh\
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@babakks : you're correct: GOOS in this case is the operating system of the runner, which isn't the target per se but it is because we Windows releases are built on the Windows runner.

That said, I think the before hooks above short circuit the make commands on the Windows runner, which would / should result in just echoing the commands without running them.

You can look back into #7324 where completions were only built for Windows and Mac OS:

cli/.goreleaser.yml

Lines 8 to 13 in 8b987e2

before:
hooks:
- >-
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make manpages GH_VERSION={{.Version}}
- >-
{{ if ne .Runtime.Goos "linux" }}echo{{ end }} make completions

If these should only be present on Windows builds, then maybe this should be conditionalized for anything not Windows

# (3-component). If we populate the `--file-version` with our semver value, then
# a zero component will be added to the end, which is not what we want.

go run github.com/tc-hib/go-winres@v0.3.3 make `
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: this project does not appear to be actively maintained if even go.mod hasn't been changed in a year.

I know this was the tool that @iamazeem used in experimenting on this issue. The only other suggestion I could offer is https://github.com/josephspurrier/goversioninfo, which has more recent releases and appears to keep up with Dependabot security updates.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I picked this one was it's simpler interface, especially regarding the setting of minimum OS version requirements. But that's a fair point. We're now using the one you mentioned. Also, now I'm thinking I don't agree with setting the min OS version.

"version": ""
},
"description": "",
"minimum-os": "win7",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest: if we continue using this dependency, then we should consider updating this to Win10

Suggested change
"minimum-os": "win7",
"minimum-os": "win10",

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I believe it's not a good idea to state the minimum OS version in the metadata. One reason is that might be breaking change to users who are already using gh on older platforms, and it's not nice to break their experience. The other reason is that, now that we're using the other library/tool, we have to manually create the manifest file (in which the OS requirement is listed), which is something that is hard to follow/maintain over time, because we'd have to update a list of UUIDs associated with different Windows versions, and I'd rather leave it.

@BagToad BagToad removed their request for review June 23, 2025 17:33
babakks added 5 commits June 24, 2025 11:46
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
@babakks
Copy link
Member Author
babakks commented Jun 24, 2025

@andyfeller I've now switched to github.com/josephspurrier/goversioninfo, and you can see the embedded metadata below (GUI and CLI). Note that, the GUI (below) shows 0.0.0.0 for the FileVersion field, but that's just a GUI behaviour, and the underlying field is set with the right value (i.e. 99.99.99 (2025-06-24)).

Via GUI

zero-file-version-gui

Via CLI

amd64 binary

PS > [System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\...\gh.exe").ToString()
File:             C:\Users\babakks\Desktop\windows\gh_99.99.99_windows_amd64\bin\gh.exe
InternalName:     gh
OriginalFilename: gh.exe
FileVersion:      99.99.99 (2025-06-24)
FileDescription:  GitHub CLI
Product:          GitHub CLI
ProductVersion:   99.99.99
Debug:            False
Patched:          False
PreRelease:       False
PrivateBuild:     False
SpecialBuild:     False
Language:         English (United States)

386 binary

PS > [System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\...\gh.exe").ToString()
File:             C:\Users\babakks\Desktop\windows\gh_99.99.99_windows_386\bin\gh.exe
InternalName:     gh
OriginalFilename: gh.exe
FileVersion:      99.99.99 (2025-06-24)
FileDescription:  GitHub CLI
Product:          GitHub CLI
ProductVersion:   99.99.99
Debug:            False
Patched:          False
PreRelease:       False
PrivateBuild:     False
SpecialBuild:     False
Language:         English (United States)

arm64 binary

PS > [System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\...\gh.exe").ToString()
File:             C:\Users\babakks\Desktop\windows\gh_99.99.99_windows_arm64\bin\gh.exe
InternalName:     gh
OriginalFilename: gh.exe
FileVersion:      99.99.99 (2025-06-24)
FileDescription:  GitHub CLI
Product:          GitHub CLI
ProductVersion:   99.99.99
Debug:            False
Patched:          False
PreRelease:       False
PrivateBuild:     False
SpecialBuild:     False
Language:         English (United States)

@babakks babakks requested a review from andyfeller June 24, 2025 14:13
Copy link
Member
@andyfeller andyfeller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, @babakks; had just a couple of questions to clarify and will ship!

@@ -12,7 +12,7 @@ before:
- >- # On linux the completions are used in nfpms below, but on macos they are used outside in the deployment build.
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make completions
- >- # We need to create the `.syso` files (per architecture) to embed Windows resources (version info)
pwsh .\script\gen-winres.ps1 386,amd64,arm64 '{{ .Version }}' .\script\winres.json .\cmd\gh\
{{ if ne .Runtime.Goos "windows" }}echo{{ end }} pwsh .\script\gen-winres.ps1 '{{ .Version }} ({{time "2006-01-02"}})' '{{ .Version }}' .\script\versioninfo.template.json .\cmd\gh\
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Is the current timestamp meant to take the place of the Build portion of the file version?

Copy link
Member Author
@babakks babakks Jun 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, as I suggested here, it will look like 2.74.0 (2025-05-29), which is similar to gh version output:

~ gh version                                                                                      
gh version 2.74.1 (2025-06-10)
https://github.com/cli/cli/releases/tag/v2.74.1

Copy link
Member
@andyfeller andyfeller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realizing my major concern about the command continuation was already addressed, this looks good to ship 👍

Co-authored-by: Andy Feller <andyfeller@github.com>
@andyfeller andyfeller enabled auto-merge June 24, 2025 21:21
@andyfeller andyfeller merged commit 2fc1e54 into trunk Jun 24, 2025
15 checks passed
@andyfeller andyfeller deleted the babakks/embed-winres-at-release branch June 24, 2025 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Version information missing from exe file
3 participants
0