From d06654dde845e90140e3e535eb71b7ffb036e26b Mon Sep 17 00:00:00 2001 From: lemz1 Date: Fri, 28 Feb 2025 20:48:09 +0100 Subject: [PATCH] add a way to download a dependency --- polymod/Polymod.hx | 28 +++++--- polymod/format/JsonHelp.hx | 20 ++++++ polymod/util/DependencyUtil.hx | 120 +++++++++++++++++++++++++++++---- 3 files changed, 147 insertions(+), 21 deletions(-) diff --git a/polymod/Polymod.hx b/polymod/Polymod.hx index b5b92f95..23702f6b 100644 --- a/polymod/Polymod.hx +++ b/polymod/Polymod.hx @@ -817,12 +817,17 @@ typedef ModContributor = }; /** - * A type representing a mod's dependencies. - * - * The map takes the mod's ID as the key and the required version as the value. - * The version follows the Semantic Versioning format, with `*.*.*` meaning any version. + * A type representing a mod's dependency. + * The `version` field is the version of the dependency + * The `url` field is the Github Repository's url to download the dependency from + * The `commit` field is the commit hash of the dependency */ -typedef ModDependencies = Map; +typedef ModDependency = +{ + version:VersionRule, + ?url:String, + ?commit:String +}; /** * A type representing data about a mod, as retrieved from its metadata file. @@ -894,14 +899,14 @@ class ModMetadata * These other mods must be also be loaded in order for this mod to load, * and this mod must be loaded after the dependencies. */ - public var dependencies:ModDependencies; + public var dependencies:Map; /** * A list of dependencies. * This mod must be loaded after the optional dependencies, * but those mods do not necessarily need to be loaded. */ - public var optionalDependencies:ModDependencies; + public var optionalDependencies:Map; /** * A deprecated field representing the mod's author. @@ -1014,8 +1019,8 @@ class ModMetadata m.license = JsonHelp.str(json, 'license'); m.metadata = JsonHelp.mapStr(json, 'metadata'); - m.dependencies = JsonHelp.mapVersionRule(json, 'dependencies'); - m.optionalDependencies = JsonHelp.mapVersionRule(json, 'optionalDependencies'); + m.dependencies = JsonHelp.mapModDependency(json, 'dependencies'); + m.optionalDependencies = JsonHelp.mapModDependency(json, 'optionalDependencies'); return m; } @@ -1166,6 +1171,11 @@ enum abstract PolymodErrorCode(String) from String to String */ var DEPENDENCY_CHECK_SKIPPED:String = 'dependency_check_skipped'; + /** + * Polymod attempted to download a mod, but it failed to do so. + */ + var DEPENDENCY_NOT_DOWNLOADABLE:String = 'dependency_not_downloadable'; + /** * The given mod's API version does not match the version rule passed to Polymod.init. * - This generally indicates the mod is outdated and should be updated by the author. diff --git a/polymod/format/JsonHelp.hx b/polymod/format/JsonHelp.hx index 5b3d0a83..c2913a73 100644 --- a/polymod/format/JsonHelp.hx +++ b/polymod/format/JsonHelp.hx @@ -1,5 +1,6 @@ package polymod.format; +import polymod.Polymod.ModDependency; import thx.semver.VersionRule; class JsonHelp @@ -89,6 +90,25 @@ class JsonHelp return map; } + public static function mapModDependency(json:Dynamic, field:String):Map + { + var map:Map = new Map(); + if (json == null || field == '' || field == null) + return map; + var val = null; + if (Reflect.hasField(json, field)) + val = Reflect.field(json, field); + if (val != null) + { + for (field in Reflect.fields(val)) + { + var fieldVal = Reflect.field(val, field); + map.set(field, fieldVal); + } + } + return map; + } + public static function str(json:Dynamic, field:String, defaultValue:String = ''):String { var str:String = ''; diff --git a/polymod/util/DependencyUtil.hx b/polymod/util/DependencyUtil.hx index dd8818be..799691d1 100644 --- a/polymod/util/DependencyUtil.hx +++ b/polymod/util/DependencyUtil.hx @@ -1,7 +1,12 @@ package polymod.util; -import polymod.Polymod.ModDependencies; +import haxe.Http; +import haxe.io.Bytes; +import haxe.zip.Reader; +import polymod.Polymod.ModDependency; import polymod.Polymod.ModMetadata; +import sys.FileSystem; +import sys.io.File; import thx.semver.VersionRule; /** @@ -53,7 +58,7 @@ class DependencyUtil var result:Array = []; // Compile a map of mod dependencies. - var deps:ModDependencies = compileDependencies(modList); + var deps:Map = compileDependencies(modList); // Check that all mods are in the mod list. var relevantMods:Array = []; @@ -68,7 +73,7 @@ class DependencyUtil // Check that all dependencies are satisfied. for (dep in deps.keys()) { - var depRule:VersionRule = deps.get(dep); + var depRule:VersionRule = deps.get(dep).version; // Check that the dependency is in the mod list. var depMod:ModMetadata = null; @@ -112,7 +117,7 @@ class DependencyUtil static function validateDependencies(modList:Array):Bool { // Compile a map of mod dependencies. - var deps:ModDependencies = compileDependencies(modList); + var deps:Map = compileDependencies(modList); // Check that all mods are in the mod list. var relevantMods:Array = []; @@ -127,7 +132,7 @@ class DependencyUtil // Check that all dependencies are satisfied. for (dep in deps.keys()) { - var depRule:VersionRule = deps.get(dep); + var depRule:VersionRule = deps.get(dep).version; // Check that the dependency is in the mod list. var depMod:ModMetadata = null; @@ -307,28 +312,37 @@ class DependencyUtil * For example, if one mod requires `>1.2.0` of `modA` and another requires `>1.3.0` of `modA`, * the merged list will be `[modA: '>1.2.0 && >1.3.0']`. */ - public static function compileDependencies(modList:Array):Map + public static function compileDependencies(modList:Array):Map { - var result:Map = []; + var result:Map = []; for (mod in modList) { if (result[mod.id] == null) - result[mod.id] = VersionUtil.DEFAULT_VERSION_RULE; + result[mod.id] = { + version: VersionUtil.DEFAULT_VERSION_RULE + }; if (mod.dependencies != null) { for (dependencyId in mod.dependencies.keys()) { - var dependencyRule:VersionRule = mod.dependencies[dependencyId]; + if (result[dependencyId] == null) + result[dependencyId] = { + version: VersionUtil.DEFAULT_VERSION_RULE + }; - if (result[dependencyId] != null) + result[dependencyId].url = mod.dependencies[dependencyId].url; + result[dependencyId].commit = mod.dependencies[dependencyId].commit; + var dependencyRule:VersionRule = mod.dependencies[dependencyId].version; + + if (result[dependencyId].version != null) { - result[dependencyId] = VersionUtil.combineRulesAnd(result[dependencyId], dependencyRule); + result[dependencyId].version = VersionUtil.combineRulesAnd(result[dependencyId].version, dependencyRule); } else { - result[dependencyId] = dependencyRule; + result[dependencyId].version = dependencyRule; } } } @@ -336,4 +350,86 @@ class DependencyUtil return result; } + + /** + * Given a github url and commit hash, download the dependency. + * This is done using an http request. + * @param outputDir the directory to download the dependency to + * @param url github repository url: https://github.com/OWNER/REPOSITORY + * @param commit commit hash + */ + public static function downloadDependency(outputDir:String, url:String, commit:String):Void + { + var zipBytes:Null = getDependencyAsZip(url, commit); + + if (zipBytes == null || zipBytes.length == 0) + return; + + var reader = new Reader(new haxe.io.BytesInput(zipBytes)); + var entries = reader.read(); + + if (!FileSystem.exists(outputDir)) + { + FileSystem.createDirectory(outputDir); + } + + for (entry in entries) + { + var fileName = entry.fileName; + if (entry.fileSize > 0) + { + var content = entry.data; + var filePath = outputDir + "/" + fileName; + + var dirs = fileName.split("/"); + dirs.pop(); + var currentDir = outputDir; + for (dir in dirs) + { + currentDir += "/" + dir; + if (!FileSystem.exists(currentDir)) + { + FileSystem.createDirectory(currentDir); + } + } + + File.saveBytes(filePath, content); + trace("Extracted: " + filePath); + } + } + + trace("Extraction complete. Files saved in: " + outputDir); + } + + /** + * Get the bytes of a dependency as a zip file. + * @param url github repository url: https://github.com/OWNER/REPOSITORY + * @param commit commit hash + * @return Null + */ + public static function getDependencyAsZip(url:String, commit:String):Null + { + // url structure of the code to download + // https://codeload.github.com/OWNER/REPOSITORY/zip/COMMIT + + var urlSplit = url.split('/'); + var repository = urlSplit[urlSplit.length - 1]; + var owner = urlSplit[urlSplit.length - 2]; + + var downloadUrl = 'https://codeload.github.com/${owner}/${repository}/zip/${commit}'; + + var zipBytes:Null = null; + + var http = new Http(downloadUrl); + http.onBytes = function(bytes:Bytes) + { + zipBytes = bytes; + } + http.request(false); + + if (zipBytes == null || zipBytes.length == 0) + Polymod.error(DEPENDENCY_NOT_DOWNLOADABLE, 'Failed to download dependency: ${downloadUrl}'); + + return zipBytes; + } }