summaryrefslogtreecommitdiff
path: root/src/SMAPI.ModBuildConfig/DeployModTask.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.ModBuildConfig/DeployModTask.cs')
-rw-r--r--src/SMAPI.ModBuildConfig/DeployModTask.cs153
1 files changed, 153 insertions, 0 deletions
diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs
new file mode 100644
index 00000000..0483f651
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs
@@ -0,0 +1,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
+{
+ /// <summary>A build task which deploys the mod files and prepares a release zip.</summary>
+ public class DeployModTask : 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;
+ }
+ }
+}