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;
}
}
}