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 { /// A build task which packs mod files into a conventional release zip. public class CreateModReleaseZip : Task { /********* ** Properties *********/ /// The name of the manifest file. private readonly string ManifestFileName = "manifest.json"; /********* ** Accessors *********/ /// The mod files to pack. [Required] public ITaskItem[] Files { get; set; } /// The name of the mod. [Required] public string ModName { get; set; } /// The absolute or relative path to the folder which should contain the generated zip file. [Required] public string OutputFolderPath { get; set; } /********* ** Public methods *********/ /// When overridden in a derived class, executes the task. /// true if the task successfully executed; otherwise, false. 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; } } /// Get a semantic version from the mod manifest (if available). /// The manifest file wasn't found or is invalid. 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 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 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+ } /// Get a case-insensitive dictionary matching the given JSON. /// The JSON to parse. private IDictionary Parse(string json) { IDictionary MakeCaseInsensitive(IDictionary dict) { foreach (var field in dict.ToArray()) { if (field.Value is IDictionary value) dict[field.Key] = MakeCaseInsensitive(value); } return new Dictionary(dict, StringComparer.InvariantCultureIgnoreCase); } IDictionary data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); return MakeCaseInsensitive(data); } /// Get a copy of a filename with all invalid filename characters substituted. /// The filename. private string EscapeInvalidFilenameCharacters(string name) { foreach (char invalidChar in Path.GetInvalidFileNameChars()) name = name.Replace(invalidChar, '.'); return name; } } }