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