encapsulate mod file info in deploy task

This commit is contained in:
Jesse Plamondon-Willard
2025-04-13 19:12:48 -04:00
parent 63319f67a6
commit 064f0ca7d6
5 changed files with 121 additions and 64 deletions
+11 -20
View File
@@ -92,10 +92,10 @@ public class DeployModTask : Task
{
try
{
string manifestPath = Path.Combine(this.ProjectDir, "manifest.json");
string manifestPath = Path.Combine(this.ProjectDir, BundleFile.ManifestFileName);
if (!new JsonHelper().ReadJsonFileIfExists(manifestPath, out Manifest rawManifest))
{
this.Log.LogError("[mod build package] The mod's manifest.json file doesn't exist.");
this.Log.LogError($"[mod build package] The mod's {BundleFile.ManifestFileName} file doesn't exist.");
return false;
}
manifest = rawManifest;
@@ -104,14 +104,14 @@ public class DeployModTask : Task
{
// log the inner exception, otherwise the message will be generic
Exception exToShow = ex.InnerException ?? ex;
this.Log.LogError($"[mod build package] The mod's manifest.json file isn't valid JSON: {exToShow.Message}");
this.Log.LogError($"[mod build package] The mod's {BundleFile.ManifestFileName} file isn't valid JSON: {exToShow.Message}");
return false;
}
// validate manifest fields
if (!ManifestValidator.TryValidateFields(manifest, out string error))
{
this.Log.LogError($"[mod build package] The mod's manifest.json file is invalid: {error}");
this.Log.LogError($"[mod build package] The mod's {BundleFile.ManifestFileName} file is invalid: {error}");
return false;
}
}
@@ -301,19 +301,12 @@ public class DeployModTask : Task
{
foreach (var mod in modPackages)
{
string relativePath = modPackages.Count == 1
string folderPath = modPackages.Count == 1
? outputPath
: Path.Combine(outputPath, this.EscapeInvalidFilenameCharacters(mod.Key));
foreach (var file in mod.Value.GetFiles())
{
string fromPath = file.Value.FullName;
string toPath = Path.Combine(relativePath, file.Key);
Directory.CreateDirectory(Path.GetDirectoryName(toPath)!);
File.Copy(fromPath, toPath, overwrite: true);
}
foreach (BundleFile from in mod.Value.GetFiles())
from.CopyToFolder(folderPath);
}
}
@@ -330,21 +323,19 @@ public class DeployModTask : Task
foreach (var mod in modPackages)
{
string modFolder = this.EscapeInvalidFilenameCharacters(mod.Key);
foreach (var file in mod.Value.GetFiles())
foreach (BundleFile from in mod.Value.GetFiles())
{
string relativePath = file.Key;
string relativePath = from.RelativePath;
if (relativePath.Contains('\\'))
relativePath = string.Join("/", PathUtilities.GetSegments(relativePath)); // zip files use forward slashes regardless of OS
FileInfo fileInfo = file.Value;
string archivePath = modPackages.Count == 1
? $"{modFolder}/{relativePath}"
: $"{this.ModFolderName}/{modFolder}/{relativePath}";
using Stream fileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
using Stream fileStreamInZip = archive.CreateEntry(archivePath).Open();
fileStream.CopyTo(fileStreamInZip);
from.CopyToStream(fileStreamInZip);
}
}
}
@@ -0,0 +1,64 @@
using System;
using System.IO;
namespace StardewModdingAPI.ModBuildConfig.Framework;
/// <summary>A file that should be deployed or zipped as part of a mod.</summary>
internal class BundleFile
{
/*********
** Accessors
*********/
/// <summary>The name of the manifest file.</summary>
public const string ManifestFileName = "manifest.json";
/// <summary>The file's relative path within the mod.</summary>
public string RelativePath { get; }
/// <summary>The file to copy from.</summary>
public FileInfo File { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="relativePath">The file's relative path within the mod.</param>
/// <param name="file">The file to copy from.</param>
public BundleFile(string relativePath, FileInfo file)
{
this.RelativePath = relativePath;
this.File = file;
}
/// <summary>Get whether this entry is for the mod's <samp>manifest.json</samp> file.</summary>
public bool IsModManifest()
{
return BundleFile.IsModManifest(this.RelativePath);
}
/// <summary>Copy the file into a destination folder.</summary>
/// <param name="folderPath">The folder path to use as the base for the <see cref="RelativePath"/>.</param>
public void CopyToFolder(string folderPath)
{
string toPath = Path.Combine(folderPath, this.RelativePath);
Directory.CreateDirectory(Path.GetDirectoryName(toPath)!);
this.File.CopyTo(toPath, overwrite: true);
}
/// <summary>Copy the file's contents into a stream.</summary>
/// <param name="stream">The stream into which to write the file's contents.</param>
public void CopyToStream(Stream stream)
{
using FileStream fromStream = this.File.OpenRead();
fromStream.CopyTo(stream);
}
/// <summary>Get whether a relative path is for the mod's <samp>manifest.json</samp> file.</summary>
/// <param name="relativePath">The relative path within the mod folder within the mod's folder.</param>
public static bool IsModManifest(string relativePath)
{
return string.Equals(relativePath, BundleFile.ManifestFileName, StringComparison.OrdinalIgnoreCase);
}
}
@@ -18,11 +18,8 @@ internal class ContentPackFileManager : IModFileManager
/*********
** Fields
*********/
/// <summary>The name of the manifest file.</summary>
private readonly string ManifestFileName = "manifest.json";
/// <summary>The files that are part of the package.</summary>
private readonly Dictionary<string, FileInfo> Files = new(StringComparer.OrdinalIgnoreCase);
private readonly List<BundleFile> Files = [];
/*********
@@ -44,37 +41,43 @@ internal class ContentPackFileManager : IModFileManager
throw GetError($"that folder doesn't exist at {projectDirInfo.FullName}");
// collect files
foreach (FileInfo entry in projectDirInfo.GetFiles("*", SearchOption.AllDirectories))
BundleFile manifestEntry = null;
foreach (FileInfo file in projectDirInfo.GetFiles("*", SearchOption.AllDirectories))
{
string relativePath = PathUtilities.GetRelativePath(projectDirInfo.FullName, entry.FullName);
FileInfo file = entry;
string relativePath = PathUtilities.GetRelativePath(projectDirInfo.FullName, file.FullName);
if (!this.ShouldIgnore(file, relativePath, ignoreFilePaths, ignoreFilePatterns))
this.Files[relativePath] = file;
{
BundleFile entry = new(relativePath, file);
this.Files.Add(entry);
if (manifestEntry is null && entry.IsModManifest())
manifestEntry = entry;
}
}
// validate manifest
if (validateManifest)
{
// get manifest file
if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile))
throw GetError($"it has no {this.ManifestFileName} file");
if (manifestEntry is null)
throw GetError($"it has no {BundleFile.ManifestFileName} file");
// parse file
Manifest manifest;
try
{
new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out Manifest rawManifest);
new JsonHelper().ReadJsonFileIfExists(manifestEntry.File.FullName, out Manifest rawManifest);
manifest = rawManifest;
}
catch (JsonReaderException ex)
{
throw GetError($"its {this.ManifestFileName} file isn't valid JSON: {ex.InnerException?.Message ?? ex.Message}");
throw GetError($"its {BundleFile.ManifestFileName} file isn't valid JSON: {ex.InnerException?.Message ?? ex.Message}");
}
// validate manifest fields
if (!ManifestValidator.TryValidateFields(manifest, out string error))
throw GetError($"its {this.ManifestFileName} file is invalid: {error}");
throw GetError($"its {BundleFile.ManifestFileName} file is invalid: {error}");
// validate version
if (version == null)
@@ -82,7 +85,7 @@ internal class ContentPackFileManager : IModFileManager
if (!SemanticVersion.TryParse(version, out ISemanticVersion requiredVersion))
throw GetError($"the provided Version value '{version}' isn't a valid semantic version");
if (manifest.Version.CompareTo(requiredVersion) != 0)
throw GetError($"its {this.ManifestFileName} has version '{manifest.Version}' instead of the required '{requiredVersion}'");
throw GetError($"its {BundleFile.ManifestFileName} has version '{manifest.Version}' instead of the required '{requiredVersion}'");
}
UserErrorException GetError(string reasonPhrase)
@@ -92,9 +95,9 @@ internal class ContentPackFileManager : IModFileManager
}
///<inheritdoc/>
public IDictionary<string, FileInfo> GetFiles()
public IEnumerable<BundleFile> GetFiles()
{
return new Dictionary<string, FileInfo>(this.Files, StringComparer.OrdinalIgnoreCase);
return this.Files;
}
/// <summary>Get whether a content file should be ignored.</summary>
@@ -1,11 +1,10 @@
using System.Collections.Generic;
using System.IO;
namespace StardewModdingAPI.ModBuildConfig.Framework;
/// <summary>Manages the files that are part of a mod in the release package.</summary>
public interface IModFileManager
internal interface IModFileManager
{
/// <summary>Get the files in the mod package.</summary>
public IDictionary<string, FileInfo> GetFiles();
public IEnumerable<BundleFile> GetFiles();
}
@@ -13,14 +13,11 @@ internal class MainModFileManager : IModFileManager
/*********
** Fields
*********/
/// <summary>The name of the manifest file.</summary>
private readonly string ManifestFileName = "manifest.json";
/// <summary>The files that are part of the package.</summary>
private readonly Dictionary<string, FileInfo> Files = new(StringComparer.OrdinalIgnoreCase);
private readonly List<BundleFile> Files = [];
/// <summary>The file extensions used by assembly files.</summary>
private readonly ISet<string> AssemblyFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
private readonly HashSet<string> AssemblyFileExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".dll",
".exe",
@@ -29,8 +26,8 @@ internal class MainModFileManager : IModFileManager
};
/// <summary>The DLLs which match the <see cref="ExtraAssemblyTypes.Game"/> type.</summary>
private readonly ISet<string> GameDllNames = new HashSet<string>
{
private readonly HashSet<string> GameDllNames =
[
// SMAPI
"0Harmony",
"Mono.Cecil",
@@ -56,7 +53,7 @@ internal class MainModFileManager : IModFileManager
"Steamworks.NET",
"TextCopy",
"xTile"
};
];
/*********
@@ -80,33 +77,36 @@ internal class MainModFileManager : IModFileManager
throw new UserErrorException("Could not create mod package because no build output was found.");
// collect files
foreach (Tuple<string, FileInfo> entry in this.GetPossibleFiles(projectDir, targetDir))
BundleFile manifestEntry = null;
foreach (BundleFile entry in this.GetPossibleFiles(projectDir, targetDir))
{
string relativePath = entry.Item1;
FileInfo file = entry.Item2;
if (!this.ShouldIgnore(entry.File, entry.RelativePath, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, modDllName))
{
this.Files.Add(entry);
if (!this.ShouldIgnore(file, relativePath, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, modDllName))
this.Files[relativePath] = file;
if (manifestEntry is null && entry.IsModManifest())
manifestEntry = entry;
}
}
// check for required files
if (validateRequiredModFiles)
{
// manifest
if (!this.Files.ContainsKey(this.ManifestFileName))
throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output.");
if (manifestEntry is null)
throw new UserErrorException($"Could not create mod package because no {BundleFile.ManifestFileName} was found in the project or build output.");
// DLL
// ReSharper disable once SimplifyLinqExpression
if (!this.Files.Any(p => !p.Key.EndsWith(".dll")))
if (!this.Files.Any(p => !p.RelativePath.EndsWith(".dll")))
throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output.");
}
}
///<inheritdoc/>
public IDictionary<string, FileInfo> GetFiles()
public IEnumerable<BundleFile> GetFiles()
{
return new Dictionary<string, FileInfo>(this.Files, StringComparer.OrdinalIgnoreCase);
return this.Files;
}
@@ -117,15 +117,15 @@ internal class MainModFileManager : IModFileManager
/// <param name="projectDir">The folder containing the project files.</param>
/// <param name="targetDir">The folder containing the build output.</param>
/// <returns>Returns tuples containing the relative path within the mod folder, and the file to copy to it.</returns>
private IEnumerable<Tuple<string, FileInfo>> GetPossibleFiles(string projectDir, string targetDir)
private IEnumerable<BundleFile> GetPossibleFiles(string projectDir, string targetDir)
{
// project manifest
bool hasProjectManifest = false;
{
FileInfo manifest = new(Path.Combine(projectDir, this.ManifestFileName));
FileInfo manifest = new(Path.Combine(projectDir, BundleFile.ManifestFileName));
if (manifest.Exists)
{
yield return Tuple.Create(this.ManifestFileName, manifest);
yield return new BundleFile(BundleFile.ManifestFileName, manifest);
hasProjectManifest = true;
}
}
@@ -138,7 +138,7 @@ internal class MainModFileManager : IModFileManager
foreach (FileInfo file in translationsFolder.EnumerateFiles("*", SearchOption.AllDirectories))
{
string relativePath = PathUtilities.GetRelativePath(projectDir, file.FullName);
yield return Tuple.Create(relativePath, file);
yield return new BundleFile(relativePath, file);
}
hasProjectTranslations = true;
}
@@ -151,7 +151,7 @@ internal class MainModFileManager : IModFileManager
foreach (FileInfo file in assetsFolder.EnumerateFiles("*", SearchOption.AllDirectories))
{
string relativePath = PathUtilities.GetRelativePath(projectDir, file.FullName);
yield return Tuple.Create(relativePath, file);
yield return new BundleFile(relativePath, file);
}
hasAssetsFolder = true;
}
@@ -165,7 +165,7 @@ internal class MainModFileManager : IModFileManager
string[] segments = PathUtilities.GetSegments(relativePath);
// prefer project manifest/i18n/assets files
if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName))
if (hasProjectManifest && BundleFile.IsModManifest(relativePath))
continue;
if (hasProjectTranslations && this.EqualsInvariant(segments[0], "i18n"))
continue;
@@ -173,7 +173,7 @@ internal class MainModFileManager : IModFileManager
continue;
// add file
yield return Tuple.Create(relativePath, file);
yield return new BundleFile(relativePath, file);
}
}