Skip to content

[FEATURE] Add a way to download dependencies #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions polymod/Polymod.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, VersionRule>;
typedef ModDependency =
{
version:VersionRule,
?url:String,
?commit:String
};

/**
* A type representing data about a mod, as retrieved from its metadata file.
Expand Down Expand Up @@ -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<String, ModDependency>;

/**
* 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<String, ModDependency>;

/**
* A deprecated field representing the mod's author.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions polymod/format/JsonHelp.hx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package polymod.format;

import polymod.Polymod.ModDependency;
import thx.semver.VersionRule;

class JsonHelp
Expand Down Expand Up @@ -89,6 +90,25 @@ class JsonHelp
return map;
}

public static function mapModDependency(json:Dynamic, field:String):Map<String, ModDependency>
{
var map:Map<String, ModDependency> = new Map<String, ModDependency>();
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 = '';
Expand Down
120 changes: 108 additions & 12 deletions polymod/util/DependencyUtil.hx
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand Down Expand Up @@ -53,7 +58,7 @@ class DependencyUtil
var result:Array<ModMetadata> = [];

// Compile a map of mod dependencies.
var deps:ModDependencies = compileDependencies(modList);
var deps:Map<String, ModDependency> = compileDependencies(modList);

// Check that all mods are in the mod list.
var relevantMods:Array<ModMetadata> = [];
Expand All @@ -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;
Expand Down Expand Up @@ -112,7 +117,7 @@ class DependencyUtil
static function validateDependencies(modList:Array<ModMetadata>):Bool
{
// Compile a map of mod dependencies.
var deps:ModDependencies = compileDependencies(modList);
var deps:Map<String, ModDependency> = compileDependencies(modList);

// Check that all mods are in the mod list.
var relevantMods:Array<ModMetadata> = [];
Expand All @@ -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;
Expand Down Expand Up @@ -307,33 +312,124 @@ 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<ModMetadata>):Map<String, VersionRule>
public static function compileDependencies(modList:Array<ModMetadata>):Map<String, ModDependency>
{
var result:Map<String, VersionRule> = [];
var result:Map<String, ModDependency> = [];

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

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<Bytes> = 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<Bytes>
*/
public static function getDependencyAsZip(url:String, commit:String):Null<Bytes>
{
// 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<Bytes> = 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;
}
}