summaryrefslogtreecommitdiff
path: root/src/SMAPI.ModBuildConfig/Tasks/CreateModReleaseZip.cs
blob: b9460b39dfc0fe8931b5a7ff14073e301978ce43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Web.Script.Serialization;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using StardewModdingAPI.Common;

namespace StardewModdingAPI.ModBuildConfig.Tasks
{
    /// <summary>A build task which packs mod files into a conventional release zip.</summary>
    public class CreateModReleaseZip : Task
    {
        /*********
        ** Properties
        *********/
        /// <summary>The name of the manifest file.</summary>
        private readonly string ManifestFileName = "manifest.json";


        /*********
        ** Accessors
        *********/
        /// <summary>The mod files to pack.</summary>
        [Required]
        public ITaskItem[] Files { get; set; }

        /// <summary>The name of the mod.</summary>
        [Required]
        public string ModName { get; set; }

        /// <summary>The absolute or relative path to the folder which should contain the generated zip file.</summary>
        [Required]
        public string OutputFolderPath { get; set; }


        /*********
        ** Public methods
        *********/
        /// <summary>When overridden in a derived class, executes the task.</summary>
        /// <returns>true if the task successfully executed; otherwise, false.</returns>
        public override bool Execute()
        {
            try
            {
                // get names
                string zipName = this.EscapeInvalidFilenameCharacters($"{this.ModName}-{this.GetManifestVersion()}.zip");
                string folderName = this.EscapeInvalidFilenameCharacters(this.ModName);
                string zipPath = Path.Combine(this.OutputFolderPath, zipName);

                // create zip file
                Directory.CreateDirectory(this.OutputFolderPath);
                using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write))
                using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create))
                {
                    foreach (ITaskItem file in this.Files)
                    {
                        // get file info
                        string filePath = file.ItemSpec;
                        string entryName = folderName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension");
                        if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase))
                            entryName = Path.Combine("i18n", entryName);

                        // add to zip
                        using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                        using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open())
                        {
                            fileStream.CopyTo(fileStreamInZip);
                        }
                    }
                }

                return true;
            }
            catch (Exception ex)
            {
                this.Log.LogErrorFromException(ex);
                return false;
            }
        }

        /// <summary>Get a semantic version from the mod manifest (if available).</summary>
        /// <exception cref="InvalidOperationException">The manifest file wasn't found or is invalid.</exception>
        public string GetManifestVersion()
        {
            // find manifest file
            ITaskItem file = this.Files.FirstOrDefault(p => this.ManifestFileName.Equals(Path.GetFileName(p.ItemSpec), StringComparison.InvariantCultureIgnoreCase));
            if (file == null)
                throw new InvalidOperationException($"The mod must include a {this.ManifestFileName} file.");

            // read content
            string json = File.ReadAllText(file.ItemSpec);
            if (string.IsNullOrWhiteSpace(json))
                throw new InvalidOperationException($"The mod's {this.ManifestFileName} file must not be empty.");

            // parse JSON
            IDictionary<string, object> data;
            try
            {
                data = this.Parse(json);
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException($"The mod's {this.ManifestFileName} couldn't be parsed. It doesn't seem to be valid JSON.", ex);
            }

            // get version field
            object versionObj = data.ContainsKey("Version") ? data["Version"] : null;
            if (versionObj == null)
                throw new InvalidOperationException($"The mod's {this.ManifestFileName} must have a version field.");

            // get version string
            if (versionObj is IDictionary<string, object> versionFields) // SMAPI 1.x
            {
                int major = versionFields.ContainsKey("MajorVersion") ? (int) versionFields["MajorVersion"] : 0;
                int minor = versionFields.ContainsKey("MinorVersion") ? (int) versionFields["MinorVersion"] : 0;
                int patch = versionFields.ContainsKey("PatchVersion") ? (int) versionFields["PatchVersion"] : 0;
                string tag = versionFields.ContainsKey("Build") ? (string) versionFields["Build"] : null;
                return new SemanticVersionImpl(major, minor, patch, tag).ToString();
            }
            return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+
        }

        /// <summary>Get a case-insensitive dictionary matching the given JSON.</summary>
        /// <param name="json">The JSON to parse.</param>
        private IDictionary<string, object> Parse(string json)
        {
            IDictionary<string, object> MakeCaseInsensitive(IDictionary<string, object> dict)
            {
                foreach (var field in dict.ToArray())
                {
                    if (field.Value is IDictionary<string, object> value)
                        dict[field.Key] = MakeCaseInsensitive(value);
                }
                return new Dictionary<string, object>(dict, StringComparer.InvariantCultureIgnoreCase);
            }

            IDictionary<string, object> data = (IDictionary<string, object>)new JavaScriptSerializer().DeserializeObject(json);
            return MakeCaseInsensitive(data);
        }

        /// <summary>Get a copy of a filename with all invalid filename characters substituted.</summary>
        /// <param name="name">The filename.</param>
        private string EscapeInvalidFilenameCharacters(string name)
        {
            foreach (char invalidChar in Path.GetInvalidFileNameChars())
                name = name.Replace(invalidChar, '.');
            return name;
        }
    }
}