mirror of
https://github.com/goreleaser/nfpm.git
synced 2026-06-19 08:05:04 +00:00
fix: ensure ommitting the productcode will generate one
This commit is contained in:
+28
-4
@@ -3,6 +3,7 @@
|
||||
package msi
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
@@ -125,12 +126,22 @@ func (m *MSI) Package(info *nfpm.Info, w io.Writer) error {
|
||||
WithVersion(convertToMSIVersion(info.Version)).
|
||||
WithAllUsers(*info.MSI.AllUsers)
|
||||
|
||||
if info.MSI.ProductCode != "" {
|
||||
b = b.WithProductCode(info.MSI.ProductCode)
|
||||
// ProductCode must always be present. When omitted we derive a stable GUID
|
||||
// from the product name (kept constant across versions so it does not change
|
||||
// on every version bump).
|
||||
productCode := info.MSI.ProductCode
|
||||
if productCode == "" {
|
||||
productCode = deriveGUID("product|" + info.MSI.ProductName)
|
||||
}
|
||||
if info.MSI.UpgradeCode != "" {
|
||||
b = b.WithUpgradeCode(info.MSI.UpgradeCode)
|
||||
b = b.WithProductCode(productCode)
|
||||
|
||||
// UpgradeCode stays stable across versions; derive it from the product name
|
||||
// alone when omitted so upgrades work out of the box.
|
||||
upgradeCode := info.MSI.UpgradeCode
|
||||
if upgradeCode == "" {
|
||||
upgradeCode = deriveGUID("upgrade|" + info.MSI.ProductName)
|
||||
}
|
||||
b = b.WithUpgradeCode(upgradeCode)
|
||||
for k, v := range info.MSI.Properties {
|
||||
b = b.WithProperty(k, v)
|
||||
}
|
||||
@@ -558,6 +569,19 @@ func looksLikeGUID(s string) bool {
|
||||
return strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}")
|
||||
}
|
||||
|
||||
// deriveGUID produces a stable, braced uppercase GUID (RFC 4122 v5 style) from
|
||||
// the given seed. The same seed always yields the same GUID, keeping builds
|
||||
// reproducible.
|
||||
func deriveGUID(seed string) string {
|
||||
h := sha1.Sum([]byte("nfpm-msi:" + seed))
|
||||
var b [16]byte
|
||||
copy(b[:], h[:16])
|
||||
b[6] = (b[6] & 0x0f) | 0x50 // version 5
|
||||
b[8] = (b[8] & 0x3f) | 0x80 // RFC 4122 variant
|
||||
s := fmt.Sprintf("%X", b[:])
|
||||
return fmt.Sprintf("{%s-%s-%s-%s-%s}", s[0:8], s[8:12], s[12:16], s[16:20], s[20:32])
|
||||
}
|
||||
|
||||
// convertToMSIVersion converts a semver-style version to MSI's
|
||||
// Major.Minor.Build format. Each field is numeric and clamped to 65535.
|
||||
func convertToMSIVersion(version string) string {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -168,6 +169,26 @@ func TestExplicitGUIDs(t *testing.T) {
|
||||
packageAndValidate(t, info)
|
||||
}
|
||||
|
||||
// TestDerivedProductCode guards against shipping an MSI without a ProductCode
|
||||
// (msiexec fails such installs with error 1605). When the config omits the
|
||||
// codes, a derived braced GUID must be written into the package.
|
||||
func TestDerivedProductCode(t *testing.T) {
|
||||
info := exampleInfo()
|
||||
require.Empty(t, info.MSI.ProductCode)
|
||||
require.Empty(t, info.MSI.UpgradeCode)
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, msi.Default.Package(info, &buf))
|
||||
|
||||
guid := regexp.MustCompile(`\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}`)
|
||||
require.True(t, guid.Match(buf.Bytes()), "a derived GUID must be present in the package")
|
||||
|
||||
// Derivation must be reproducible.
|
||||
var buf2 bytes.Buffer
|
||||
require.NoError(t, msi.Default.Package(exampleInfo(), &buf2))
|
||||
require.Equal(t, buf.Bytes(), buf2.Bytes(), "builds with identical input must be reproducible")
|
||||
}
|
||||
|
||||
func TestShortcut(t *testing.T) {
|
||||
info := exampleInfo()
|
||||
info.MSI.Shortcuts = []nfpm.MSIShortcut{
|
||||
|
||||
@@ -590,8 +590,8 @@ type MSI struct {
|
||||
Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in msi nomenclature"`
|
||||
ProductName string `yaml:"product_name,omitempty" json:"product_name,omitempty" jsonschema:"title=product name,description=defaults to the package name"`
|
||||
Manufacturer string `yaml:"manufacturer" json:"manufacturer" jsonschema:"title=manufacturer/author of the product,example=My Company"`
|
||||
ProductCode string `yaml:"product_code,omitempty" json:"product_code,omitempty" jsonschema:"title=product code GUID,description=auto-derived when empty,example={12345678-1234-1234-1234-123456789ABC}"`
|
||||
UpgradeCode string `yaml:"upgrade_code,omitempty" json:"upgrade_code,omitempty" jsonschema:"title=upgrade code GUID,description=recommended for upgrades,example={ABCDEF01-2345-6789-ABCD-EF0123456789}"`
|
||||
ProductCode string `yaml:"product_code,omitempty" json:"product_code,omitempty" jsonschema:"title=product code GUID,description=derived from the product name (stable across versions) when empty,example={12345678-1234-1234-1234-123456789ABC}"`
|
||||
UpgradeCode string `yaml:"upgrade_code,omitempty" json:"upgrade_code,omitempty" jsonschema:"title=upgrade code GUID,description=derived from the product name when empty,example={ABCDEF01-2345-6789-ABCD-EF0123456789}"`
|
||||
InstallDir string `yaml:"install_dir,omitempty" json:"install_dir,omitempty" jsonschema:"title=default install folder name,description=defaults to the product name"`
|
||||
AllUsers *bool `yaml:"all_users,omitempty" json:"all_users,omitempty" jsonschema:"title=per-machine install,description=defaults to true"`
|
||||
Properties map[string]string `yaml:"properties,omitempty" json:"properties,omitempty" jsonschema:"title=arbitrary MSI Property rows"`
|
||||
|
||||
@@ -634,10 +634,12 @@ msi:
|
||||
# Manufacturer/author of the product. (required)
|
||||
manufacturer: "My Company"
|
||||
|
||||
# Product code GUID. Auto-derived (stable) when omitted.
|
||||
# Product code GUID. When omitted, a stable GUID is derived from the product
|
||||
# name (kept constant across versions).
|
||||
product_code: "{12345678-1234-1234-1234-123456789ABC}"
|
||||
|
||||
# Upgrade code GUID. Recommended so future versions upgrade in place.
|
||||
# Upgrade code GUID. When omitted, a stable GUID is derived from the product
|
||||
# name (kept constant across versions so upgrades work).
|
||||
upgrade_code: "{ABCDEF01-2345-6789-ABCD-EF0123456789}"
|
||||
|
||||
# Name of the default install folder (defaults to product_name).
|
||||
|
||||
@@ -677,7 +677,7 @@
|
||||
"product_code": {
|
||||
"type": "string",
|
||||
"title": "product code GUID",
|
||||
"description": "auto-derived when empty",
|
||||
"description": "derived from the product name (stable across versions) when empty",
|
||||
"examples": [
|
||||
"{12345678-1234-1234-1234-123456789ABC}"
|
||||
]
|
||||
@@ -685,7 +685,7 @@
|
||||
"upgrade_code": {
|
||||
"type": "string",
|
||||
"title": "upgrade code GUID",
|
||||
"description": "recommended for upgrades",
|
||||
"description": "derived from the product name when empty",
|
||||
"examples": [
|
||||
"{ABCDEF01-2345-6789-ABCD-EF0123456789}"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user