150 lines
5.3 KiB
Go
150 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
type versionSourceConfig struct {
|
|
GameVersion string `json:"game_version"`
|
|
ClientVersion string `json:"client_version"`
|
|
ServerVersion string `json:"server_version"`
|
|
GameDataVersion string `json:"game_data_version"`
|
|
BundleVersion string `json:"bundle_version"`
|
|
SeedDirectory string `json:"seed_directory"`
|
|
Plugins struct {
|
|
LocalIdentity string `json:"local_identity"`
|
|
CaptureEnvironment string `json:"capture_environment"`
|
|
LoginUI string `json:"login_ui"`
|
|
CashShop string `json:"cash_shop"`
|
|
} `json:"plugins"`
|
|
}
|
|
|
|
func (t task) generateVersionSource(args []string) error {
|
|
options := flag.NewFlagSet("version-source", flag.ContinueOnError)
|
|
options.SetOutput(io.Discard)
|
|
config := options.String("config", "", "version configuration")
|
|
output := options.String("output", "", "generated C# source")
|
|
plugin := options.String("plugin", "", "plugin version key")
|
|
if err := options.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if options.NArg() != 0 || *config == "" || *output == "" || *plugin == "" {
|
|
return errors.New("version-source requires --config, --output and --plugin, with no positional arguments")
|
|
}
|
|
var versions versionSourceConfig
|
|
if err := readJSON(*config, &versions, true); err != nil {
|
|
return err
|
|
}
|
|
pluginVersion, err := versions.validate(*plugin)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", *config, err)
|
|
}
|
|
configPath, err := filepath.Abs(*config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
outputPath, err := filepath.Abs(*output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if outputPath == configPath {
|
|
return errors.New("version-source output must not replace the version configuration")
|
|
}
|
|
configInfo, err := os.Stat(configPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if outputInfo, statErr := os.Stat(outputPath); statErr == nil && os.SameFile(configInfo, outputInfo) {
|
|
return errors.New("version-source output must not replace the version configuration")
|
|
} else if statErr != nil && !errors.Is(statErr, os.ErrNotExist) {
|
|
return statErr
|
|
}
|
|
source := fmt.Sprintf(`// Generated from versions.json. Do not edit.
|
|
namespace Bd2Build
|
|
{
|
|
internal static class Versions
|
|
{
|
|
internal const string Game = "%s";
|
|
internal const string ClientRelease = "%s";
|
|
internal const string GameData = "%s";
|
|
internal const string Bundle = "%s";
|
|
internal const string Plugin = "%s";
|
|
}
|
|
}
|
|
`, versions.GameVersion, versions.ClientVersion, versions.GameDataVersion, versions.BundleVersion, pluginVersion)
|
|
return writeVersionSource(outputPath, []byte(source))
|
|
}
|
|
|
|
func (c versionSourceConfig) validate(plugin string) (string, error) {
|
|
plugins := map[string]string{"local_identity": c.Plugins.LocalIdentity, "capture_environment": c.Plugins.CaptureEnvironment, "login_ui": c.Plugins.LoginUI, "cash_shop": c.Plugins.CashShop}
|
|
selected, found := plugins[plugin]
|
|
if !found {
|
|
return "", fmt.Errorf("unsupported plugin %q", plugin)
|
|
}
|
|
semver := regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+$`)
|
|
resource := regexp.MustCompile(`^[0-9]{14}$`)
|
|
if !semver.MatchString(c.GameVersion) {
|
|
return "", errors.New("game_version must be a numeric three-part version")
|
|
}
|
|
for _, key := range []string{"local_identity", "capture_environment", "login_ui", "cash_shop"} {
|
|
if !semver.MatchString(plugins[key]) {
|
|
return "", fmt.Errorf("plugins.%s must be a numeric three-part version", key)
|
|
}
|
|
}
|
|
for _, release := range []struct{ name, value, component string }{{"client_version", c.ClientVersion, "client"}, {"server_version", c.ServerVersion, "server"}} {
|
|
prefix := c.GameVersion + "+" + release.component + "."
|
|
if suffix, found := strings.CutPrefix(release.value, prefix); !found || !semver.MatchString(suffix) {
|
|
return "", fmt.Errorf("%s must be %sX.Y.Z", release.name, prefix)
|
|
}
|
|
}
|
|
for _, version := range []struct{ name, value string }{{"game_data_version", c.GameDataVersion}, {"bundle_version", c.BundleVersion}} {
|
|
if !resource.MatchString(version.value) {
|
|
return "", fmt.Errorf("%s must be a 14-digit version", version.name)
|
|
}
|
|
}
|
|
seed := filepath.Clean(filepath.FromSlash(strings.ReplaceAll(c.SeedDirectory, `\`, "/")))
|
|
if c.SeedDirectory == "" || filepath.IsAbs(seed) || strings.Contains(c.SeedDirectory, ":") || seed == ".." || strings.HasPrefix(seed, ".."+string(filepath.Separator)) {
|
|
return "", errors.New("seed_directory must be a non-empty relative path below the version file")
|
|
}
|
|
return selected, nil
|
|
}
|
|
|
|
func writeVersionSource(path string, source []byte) (err error) {
|
|
current, err := os.ReadFile(path)
|
|
if err == nil && bytes.Equal(current, source) {
|
|
return nil
|
|
}
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
file, err := os.CreateTemp(filepath.Dir(path), ".version-source-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
temporary := file.Name()
|
|
defer func() {
|
|
if removeErr := os.Remove(temporary); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
|
|
err = errors.Join(err, removeErr)
|
|
}
|
|
}()
|
|
_, writeErr := file.Write(source)
|
|
if writeErr == nil {
|
|
writeErr = file.Sync()
|
|
}
|
|
if closeErr := file.Close(); writeErr != nil || closeErr != nil {
|
|
return errors.Join(writeErr, closeErr)
|
|
}
|
|
return os.Rename(temporary, path)
|
|
}
|