From f4d1df57ee74c99de20ae2ac67bf3ab046d5566c Mon Sep 17 00:00:00 2001 From: Joshua Granick Date: Thu, 18 May 2017 11:20:53 -0700 Subject: [PATCH] Embed a custom copy of haxelib, fix 'haxelib path' --- lime/project/HXProject.hx | 11 +- lime/tools/helpers/AssetHelper.hx | 2 +- lime/tools/helpers/CPPHelper.hx | 4 +- lime/tools/helpers/CSHelper.hx | 2 +- lime/tools/helpers/HaxelibHelper.hx | 31 +- lime/tools/platforms/MacPlatform.hx | 3 +- tools/CommandLineTools.hx | 21 +- tools/haxelib/Data.hx | 360 ++++++ tools/haxelib/SemVer.hx | 177 +++ tools/haxelib/SiteApi.hx | 41 + tools/haxelib/Validator.hx | 177 +++ tools/haxelib/client/Cli.hx | 46 + tools/haxelib/client/ConvertXml.hx | 96 ++ tools/haxelib/client/FsUtils.hx | 131 +++ tools/haxelib/client/Main.hx | 1607 +++++++++++++++++++++++++++ tools/haxelib/client/Vcs.hx | 378 +++++++ tools/haxelib/server/FileStorage.hx | 284 +++++ tools/haxelib/server/Paths.hx | 54 + tools/haxelib/server/Repo.hx | 368 ++++++ tools/haxelib/server/SiteDb.hx | 159 +++ tools/tools.hxml | 7 +- tools/utils/PlatformSetup.hx | 14 +- 22 files changed, 3943 insertions(+), 30 deletions(-) create mode 100644 tools/haxelib/Data.hx create mode 100644 tools/haxelib/SemVer.hx create mode 100644 tools/haxelib/SiteApi.hx create mode 100644 tools/haxelib/Validator.hx create mode 100644 tools/haxelib/client/Cli.hx create mode 100644 tools/haxelib/client/ConvertXml.hx create mode 100644 tools/haxelib/client/FsUtils.hx create mode 100644 tools/haxelib/client/Main.hx create mode 100644 tools/haxelib/client/Vcs.hx create mode 100644 tools/haxelib/server/FileStorage.hx create mode 100644 tools/haxelib/server/Paths.hx create mode 100644 tools/haxelib/server/Repo.hx create mode 100644 tools/haxelib/server/SiteDb.hx diff --git a/lime/project/HXProject.hx b/lime/project/HXProject.hx index 9dd47235b..fa7968927 100644 --- a/lime/project/HXProject.hx +++ b/lime/project/HXProject.hx @@ -1124,15 +1124,6 @@ class HXProject { #if lime - // TODO: Need to be able to handle this without 'haxelib path' - - if (version == HaxelibHelper.getVersion (new Haxelib (haxelib.name))) { - - // Fix case where using dev directory newer than other versions - name = haxelib.name; - - } - if (HaxelibHelper.pathOverrides.exists (name)) { var param = "-cp " + HaxelibHelper.pathOverrides.get (name); @@ -1147,7 +1138,7 @@ class HXProject { try { - output = ProcessHelper.runProcess ("", "haxelib", [ "path", name ], true, true, true); + output = HaxelibHelper.runProcess ("", [ "path", name ], true, true, true); } catch (e:Dynamic) { } diff --git a/lime/tools/helpers/AssetHelper.hx b/lime/tools/helpers/AssetHelper.hx index 974f3a847..54efaa6fe 100644 --- a/lime/tools/helpers/AssetHelper.hx +++ b/lime/tools/helpers/AssetHelper.hx @@ -242,7 +242,7 @@ class AssetHelper { } - ProcessHelper.runCommand ("", "haxelib", args); + HaxelibHelper.runCommand ("", args); if (FileSystem.exists (outputFile)) { diff --git a/lime/tools/helpers/CPPHelper.hx b/lime/tools/helpers/CPPHelper.hx index cf0cab4d5..a35bfb1f8 100644 --- a/lime/tools/helpers/CPPHelper.hx +++ b/lime/tools/helpers/CPPHelper.hx @@ -132,7 +132,7 @@ class CPPHelper { Sys.putEnv ("HXCPP_EXIT_ON_ERROR", ""); - var code = ProcessHelper.runCommand (path, "haxelib", args); + var code = HaxelibHelper.runCommand (path, args); if (code != 0) { @@ -322,7 +322,7 @@ class CPPHelper { Sys.putEnv ("HXCPP_EXIT_ON_ERROR", ""); - ProcessHelper.runCommand (path, "haxelib", args); + HaxelibHelper.runCommand (path, args); } diff --git a/lime/tools/helpers/CSHelper.hx b/lime/tools/helpers/CSHelper.hx index cc4dc8a0b..7266e1702 100644 --- a/lime/tools/helpers/CSHelper.hx +++ b/lime/tools/helpers/CSHelper.hx @@ -259,7 +259,7 @@ class CSHelper { var args = [ "run", project.config.getString ("cs.buildLibrary", "hxcs"), buildFile, "--arch", arch, "--platform", platform, "--out", outPath, "--unsafe" ]; if (noCompile) args.push ("--no-compile"); - var code = ProcessHelper.runCommand (path, "haxelib", args); + var code = HaxelibHelper.runCommand (path, args); if (code != 0) { diff --git a/lime/tools/helpers/HaxelibHelper.hx b/lime/tools/helpers/HaxelibHelper.hx index 35d48ce6a..7d160801f 100644 --- a/lime/tools/helpers/HaxelibHelper.hx +++ b/lime/tools/helpers/HaxelibHelper.hx @@ -17,6 +17,7 @@ class HaxelibHelper { private static var repositoryPath:String; private static var paths = new Map (); + private static var toolPath = null; private static var versions = new Map (); @@ -90,8 +91,7 @@ class HaxelibHelper { var cacheDryRun = ProcessHelper.dryRun; ProcessHelper.dryRun = false; - - output = ProcessHelper.runProcess (Sys.getEnv ("HAXEPATH"), "haxelib", [ "config" ], true, true, true); + output = HaxelibHelper.runProcess ("", [ "config" ], true, true, true); if (output == null) output = ""; ProcessHelper.dryRun = cacheDryRun; @@ -329,6 +329,19 @@ class HaxelibHelper { } + public static function getToolPath ():String { + + if (toolPath == null) { + + toolPath = PathHelper.combine (pathOverrides.get ("lime-tools"), "haxelib.n"); + + } + + return toolPath; + + } + + public static function getVersion (haxelib:Haxelib = null):Version { var clearCache = false; @@ -353,6 +366,20 @@ class HaxelibHelper { } + public static function runCommand (path:String, args:Array, safeExecute:Bool = true, ignoreErrors:Bool = false, print:Bool = false):Int { + + return ProcessHelper.runCommand (path, "neko", [ getToolPath () ].concat (args), safeExecute, ignoreErrors, print); + + } + + + public static function runProcess (path:String, args:Array, waitForOutput:Bool = true, safeExecute:Bool = true, ignoreErrors:Bool = false, print:Bool = false, returnErrorValue:Bool = false):String { + + return ProcessHelper.runProcess (path, "neko", [ getToolPath () ].concat (args), waitForOutput, safeExecute, ignoreErrors, print, returnErrorValue); + + } + + public static function setOverridePath (haxelib:Haxelib, path:String):Void { var name = haxelib.name; diff --git a/lime/tools/platforms/MacPlatform.hx b/lime/tools/platforms/MacPlatform.hx index d103e34b4..5718601a4 100644 --- a/lime/tools/platforms/MacPlatform.hx +++ b/lime/tools/platforms/MacPlatform.hx @@ -8,6 +8,7 @@ import lime.tools.helpers.CSHelper; import lime.tools.helpers.DeploymentHelper; import lime.tools.helpers.GUID; import lime.tools.helpers.FileHelper; +import lime.tools.helpers.HaxelibHelper; import lime.tools.helpers.IconHelper; import lime.tools.helpers.JavaHelper; import lime.tools.helpers.LogHelper; @@ -116,7 +117,7 @@ class MacPlatform extends PlatformTarget { if (noOutput) return; - ProcessHelper.runCommand (targetDirectory + "/obj", "haxelib", [ "run", "hxjava", "hxjava_build.txt", "--haxe-version", "3103" ]); + HaxelibHelper.runCommand (targetDirectory + "/obj", [ "run", "hxjava", "hxjava_build.txt", "--haxe-version", "3103" ]); FileHelper.recursiveCopy (targetDirectory + "/obj/lib", PathHelper.combine (executableDirectory, "lib")); FileHelper.copyFile (targetDirectory + "/obj/ApplicationMain" + (project.debug ? "-Debug" : "") + ".jar", PathHelper.combine (executableDirectory, project.app.file + ".jar")); JavaHelper.copyLibraries (project.templatePaths, "Mac" + (is64 ? "64" : ""), executableDirectory); diff --git a/tools/CommandLineTools.hx b/tools/CommandLineTools.hx index 28664c0a8..80727235c 100644 --- a/tools/CommandLineTools.hx +++ b/tools/CommandLineTools.hx @@ -59,7 +59,7 @@ class CommandLineTools { overrides = new HXProject (); overrides.architectures = []; - HaxelibHelper.setOverridePath (new Haxelib ("lime-tools"), PathHelper.combine (HaxelibHelper.getPath (new Haxelib ("lime")), "tools")); + //HaxelibHelper.setOverridePath (new Haxelib ("lime-tools"), PathHelper.combine (HaxelibHelper.getPath (new Haxelib ("lime")), "tools")); processArguments (); version = HaxelibHelper.getVersion (); @@ -602,7 +602,7 @@ class CommandLineTools { } else { - ProcessHelper.runCommand ("", "haxelib", [ "run", handler ].concat (args)); + HaxelibHelper.runCommand ("", [ "run", handler ].concat (args)); } @@ -1813,6 +1813,8 @@ class CommandLineTools { if (FileSystem.exists (lastArgument) && FileSystem.isDirectory (lastArgument)) { + HaxelibHelper.setOverridePath (new Haxelib ("lime-tools"), PathHelper.combine (Sys.getCwd (), "tools")); + Sys.setCwd (lastArgument); runFromHaxelib = true; @@ -1826,16 +1828,25 @@ class CommandLineTools { if (!runFromHaxelib) { + var path = null; + if (FileSystem.exists ("tools.n")) { - HaxelibHelper.setOverridePath (new Haxelib ("lime"), PathHelper.combine (Sys.getCwd (), "../")); + path = PathHelper.combine (Sys.getCwd (), "../"); } else if (FileSystem.exists ("run.n")) { - HaxelibHelper.setOverridePath (new Haxelib ("lime"), Sys.getCwd ()); + path = Sys.getCwd (); + + } else { + + LogHelper.error ("Could not run Lime tools from this directory"); } + HaxelibHelper.setOverridePath (new Haxelib ("lime"), path); + HaxelibHelper.setOverridePath (new Haxelib ("lime-tools"), PathHelper.combine (path, "tools")); + } var catchArguments = false; @@ -2161,7 +2172,7 @@ class CommandLineTools { if (path != null && path != "") { - ProcessHelper.runCommand ("", "haxelib", [ "remove", name ]); + HaxelibHelper.runCommand ("", [ "remove", name ]); } diff --git a/tools/haxelib/Data.hx b/tools/haxelib/Data.hx new file mode 100644 index 000000000..6becc1d8b --- /dev/null +++ b/tools/haxelib/Data.hx @@ -0,0 +1,360 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib; + +import haxe.ds.Option; +import haxe.zip.Reader; +import haxe.zip.Entry; +import haxe.Json; +import haxelib.Validator; + +using StringTools; + +typedef UserInfos = { + var name : String; + var fullname : String; + var email : String; + var projects : Array; +} + +typedef VersionInfos = { + var date : String; + var name : SemVer;//TODO: this should eventually be called `number` + var downloads : Int; + var comments : String; +} + +typedef ProjectInfos = { + var name : String; + var desc : String; + var website : String; + var owner : String; + var license : String; + var curversion : String; + var downloads : Int; + var versions : Array; + var tags : List; +} + +abstract DependencyVersion(String) to String from SemVer { + inline function new(s:String) + this = s; + + @:to function toValidatable():Validatable + return + if (this == '') { validate: function () return None } + else @:privateAccess new SemVer(this); + + static public function isValid(s:String) + return new DependencyVersion(s).toValidatable().validate() == None; + + static public var DEFAULT(default, null) = new DependencyVersion(''); +} + +abstract Dependencies(Dynamic) from Dynamic { + @:to public function toArray():Array { + var fields = Reflect.fields(this); + fields.sort(Reflect.compare); + + var result:Array = new Array(); + + for (f in fields) { + var value:String = Reflect.field(this, f); + + var isGit = value != null && (value + "").startsWith("git:"); + + if ( !isGit ) + { + result.push ({ + name: f, + type: (DependencyType.Haxelib : DependencyType), + version: (cast value : DependencyVersion), + url: (null : String), + subDir: (null : String), + branch: (null : String), + }); + } + else + { + value = value.substr(4); + var urlParts = value.split("#"); + var url = urlParts[0]; + var branch = urlParts.length > 1 ? urlParts[1] : null; + + result.push ({ + name: f, + type: (DependencyType.Git : DependencyType), + version: (DependencyVersion.DEFAULT : DependencyVersion), + url: (url : String), + subDir: (null : String), + branch: (branch : String), + }); + } + + + } + + return result; + } + public inline function iterator() + return toArray().iterator(); +} + +@:enum abstract DependencyType(String) { + var Haxelib = null; + var Git = 'git'; + var Mercurial = 'hg'; +} + +typedef Dependency = { + name : String, + ?version : DependencyVersion, + ?type: DependencyType, + ?url: String, + ?subDir: String, + ?branch: String, +} + +typedef Infos = { + // IMPORTANT: if you change this or its fields types, + // make sure to update `schema.json` file accordingly, + // and make an update PR to https://github.com/SchemaStore/schemastore + var name : ProjectName; + @:optional var url : String; + @:optional var description : String; + var license : License; + var version : SemVer; + @:optional var classPath : String; + var releasenote : String; + @:requires('Specify at least one contributor' => _.length > 0) + var contributors : Array; + @:optional var tags : Array; + @:optional var dependencies : Dependencies; + @:optional var main:String; +} + +@:enum abstract License(String) to String { + var Gpl = 'GPL'; + var Lgpl = 'LGPL'; + var Mit = 'MIT'; + var Bsd = 'BSD'; + var Public = 'Public'; + var Apache = 'Apache'; +} + +abstract ProjectName(String) to String { + static var RESERVED_NAMES = ["haxe", "all"]; + static var RESERVED_EXTENSIONS = ['.zip', '.hxml']; + inline function new(s:String) + this = s; + + @:to function toValidatable():Validatable + return { + validate: + function ():Option { + for (r in rules) + if (!r.check(this)) + return Some(r.msg.replace('%VALUE', '`' + Json.stringify(this) + '`')); + return None; + } + } + + static var rules = {//using an array because order might matter + var a = new Array<{ msg: String, check:String->Bool }>(); + + function add(m, r) + a.push( { msg: m, check: r } ); + + add("%VALUE is not a String", Std.is.bind(_, String)); + add("%VALUE is too short", function (s) return s.length >= 3); + add("%VALUE contains invalid characters", Data.alphanum.match); + add("%VALUE is a reserved name", function(s) return RESERVED_NAMES.indexOf(s.toLowerCase()) == -1); + add("%VALUE ends with a reserved suffix", function(s) { + s = s.toLowerCase(); + for (ext in RESERVED_EXTENSIONS) + if (s.endsWith(ext)) return false; + return true; + }); + + a; + } + + public function validate() + return toValidatable().validate(); + + static public function ofString(s:String) + return switch new ProjectName(s) { + case _.toValidatable().validate() => Some(e): throw e; + case v: v; + } + + static public var DEFAULT(default, null) = new ProjectName('unknown'); +} + +class Data { + + public static var JSON(default, null) = "haxelib.json"; + public static var DOCXML(default, null) = "haxedoc.xml"; + public static var REPOSITORY(default, null) = "files/3.0"; + public static var alphanum(default, null) = ~/^[A-Za-z0-9_.-]+$/; + + + public static function safe( name : String ) { + if( !alphanum.match(name) ) + throw "Invalid parameter : "+name; + return name.split(".").join(","); + } + + public static function unsafe( name : String ) { + return name.split(",").join("."); + } + + public static function fileName( lib : String, ver : String ) { + return safe(lib)+"-"+safe(ver)+".zip"; + } + + static public function getLatest(info:ProjectInfos, ?preview:SemVer.Preview->Bool):Null { + if (info.versions.length == 0) return null; + if (preview == null) + preview = function (p) return p == null; + + var versions = info.versions.copy(); + versions.sort(function (a, b) return -SemVer.compare(a.name, b.name)); + + for (v in versions) + if (preview(v.name.preview)) return v.name; + + return versions[0].name; + } + + /** + Return the directory that contains *haxelib.json*. + If it is at the root, `""`. + If it is in a folder, the path including a trailing slash is returned. + */ + public static function locateBasePath( zip : List ):String { + var f = getJson(zip); + return f.fileName.substr(0, f.fileName.length - JSON.length); + } + + static function getJson(zip:List) + return switch topmost(zip, fileNamed(JSON)) { + case Some(f): f; + default: throw 'No $JSON found'; + } + + static function fileNamed(name:String) + return function (f:Entry) return f.fileName.endsWith(name); + + static function topmost(zip:List, predicate:Entry->Bool):Option { + var best:Entry = null, + bestDepth = 0xFFFF; + + for (f in zip) + if (predicate(f)) { + var depth = f.fileName.replace('\\', '/').split('/').length;//TODO: consider Path.normalize + if ((depth == bestDepth && f.fileName < best.fileName) || depth < bestDepth) { + best = f; + bestDepth = depth; + } + } + + return + if (best == null) + None; + else + Some(best); + } + + public static function readDoc( zip : List ) : Null + return switch topmost(zip, fileNamed(DOCXML)) { + case Some(f): Reader.unzip(f).toString(); + case None: null; + } + + + public static function readInfos( zip : List, check : Bool ) : Infos + return readData(Reader.unzip(getJson(zip)).toString(), check); + + public static function checkClassPath( zip : List, infos : Infos ) { + if ( infos.classPath != "" ) { + var basePath = Data.locateBasePath(zip); + var cp = basePath + infos.classPath; + + for( f in zip ) { + if( StringTools.startsWith(f.fileName,cp) ) + return; + } + throw 'Class path `${infos.classPath}` not found'; + } + } + + public static function readData( jsondata: String, check : Bool ) : Infos { + var doc:Infos = + try Json.parse(jsondata) + catch ( e : Dynamic ) + if (check) + throw 'JSON parse error: $e'; + else { + name : ProjectName.DEFAULT, + url : '', + version : SemVer.DEFAULT, + releasenote: 'No haxelib.json found', + license: Mit, + description: 'No haxelib.json found', + contributors: [], + } + + if (check) + Validator.validate(doc); + else { + if (!doc.version.valid) + doc.version = SemVer.DEFAULT; + } + + //TODO: we have really weird ways to go about nullability and defaults + + if (doc.dependencies == null) + doc.dependencies = {}; + + for (dep in doc.dependencies) + if (!DependencyVersion.isValid(dep.version)) + Reflect.setField(doc.dependencies, dep.name, DependencyVersion.DEFAULT);//TODO: this is pure evil + + if (doc.classPath == null) + doc.classPath = ''; + + if (doc.name.validate() != None) + doc.name = ProjectName.DEFAULT; + + if (doc.description == null) + doc.description = ''; + + if (doc.tags == null) + doc.tags = []; + + if (doc.url == null) + doc.url = ''; + + return doc; + } +} \ No newline at end of file diff --git a/tools/haxelib/SemVer.hx b/tools/haxelib/SemVer.hx new file mode 100644 index 000000000..9a45cb158 --- /dev/null +++ b/tools/haxelib/SemVer.hx @@ -0,0 +1,177 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib; + +import haxe.ds.Option; + +using Std; + +enum Preview { + ALPHA; + BETA; + RC; +} + +abstract SemVer(String) to String { + + public var major(get, never):Int; + public var minor(get, never):Int; + public var patch(get, never):Int; + public var preview(get, never):Null; + public var previewNum(get, never):Null; + public var data(get, never):SemVerData; + public var valid(get, never):Bool; + + inline function new(s) this = s; + + static public function compare(a:SemVer, b:SemVer) { + function toArray(data:SemVerData) + return [ + data.major, + data.minor, + data.patch, + if (data.preview == null) 100 else data.preview.getIndex(), + if (data.previewNum == null) -1 else data.previewNum + ]; + + var a = toArray(a.data), + b = toArray(b.data); + + for (i in 0...a.length) + switch Reflect.compare(a[i], b[i]) { + case 0: + case v: return v; + } + + return 0; + } + @:to public function toValidatable():Validator.Validatable + return { + validate: + function ():Option + return + try { + get_data(); + None; + } + catch (e:Dynamic) + Some(Std.string(e)) + } + + inline function get_major() + return data.major; + + inline function get_minor() + return data.minor; + + inline function get_patch() + return data.patch; + + inline function get_preview() + return data.preview; + + inline function get_previewNum() + return data.previewNum; + + inline function get_valid() + return isValid(this); + + @:op(a > b) static inline function gt(a:SemVer, b:SemVer) + return compare(a, b) == 1; + + @:op(a >= b) static inline function gteq(a:SemVer, b:SemVer) + return compare(a, b) != -1; + + @:op(a < b) static inline function lt(a:SemVer, b:SemVer) + return compare(a, b) == -1; + + @:op(a <= b) static inline function lteq(a:SemVer, b:SemVer) + return compare(a, b) != 1; + + @:op(a == b) static inline function eq(a:SemVer, b:SemVer) + return compare(a, b) == 0; + + @:op(a != b) static inline function neq(a:SemVer, b:SemVer) + return compare(a, b) != 0; + + static var FORMAT = ~/^(\d|[1-9]\d*)\.(\d|[1-9]\d*)\.(\d|[1-9]\d*)(-(alpha|beta|rc)(\.(\d|[1-9]\d*))?)?$/; + + static var cache = new Map(); + + @:to function get_data():SemVerData { + if (!cache.exists(this)) + cache[this] = getData(); + return cache[this]; + } + + @:from static function fromData(data:SemVerData) + return + new SemVer( + data.major + '.' + data.minor + '.' + data.patch + + if (data.preview == null) '' + else '-' + data.preview.getName().toLowerCase() + + if (data.previewNum == null) ''; + else '.' + data.previewNum + ); + + function getData():SemVerData + return + if (valid) {//RAPTORS: This query will already cause the matching. + major: FORMAT.matched(1).parseInt(), + minor: FORMAT.matched(2).parseInt(), + patch: FORMAT.matched(3).parseInt(), + preview: + switch FORMAT.matched(5) { + case 'alpha': ALPHA; + case 'beta': BETA; + case 'rc': RC; + case v if (v == null): null; + case v: throw 'unrecognized preview tag $v'; + }, + previewNum: + switch FORMAT.matched(7) { + case null: null; + case v: v.parseInt(); + } + } + else + throw '$this is not a valid version string';//TODO: include some URL for reference + + static public function isValid(s:String) + return Std.is(s, String) && FORMAT.match(s.toLowerCase()); + + static public function ofString(s:String) { + var ret = new SemVer(s); + ret.getData(); + return ret; + } + + static public var DEFAULT(default, null) = new SemVer('0.0.0'); +} + +typedef SemVerData = { + major:Int, + minor:Int, + patch:Int, + preview:Null, + previewNum:Null, +} diff --git a/tools/haxelib/SiteApi.hx b/tools/haxelib/SiteApi.hx new file mode 100644 index 000000000..2bb1f858b --- /dev/null +++ b/tools/haxelib/SiteApi.hx @@ -0,0 +1,41 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib; + +import haxelib.Data; + +interface SiteApi { + public function search( word : String ) : List<{ id : Int, name : String }>; + public function infos( project : String ) : ProjectInfos; + public function getLatestVersion( project : String ) : SemVer; + public function user( name : String ) : UserInfos; + public function register( name : String, pass : String, mail : String, fullname : String ) : Void; + public function isNewUser( name : String ) : Bool; + public function checkDeveloper( prj : String, user : String ) : Void; + public function checkPassword( user : String, pass : String ) : Bool; + public function getSubmitId() : String; + + public function processSubmit( id : String, user : String, pass : String ) : String; + + public function postInstall( project : String, version : String):Void; +} + diff --git a/tools/haxelib/Validator.hx b/tools/haxelib/Validator.hx new file mode 100644 index 000000000..d14a86e73 --- /dev/null +++ b/tools/haxelib/Validator.hx @@ -0,0 +1,177 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib; + +import haxe.ds.Option; + +#if macro +import haxe.macro.Expr; +import haxe.macro.Type; +import haxe.macro.Context; + +using haxe.macro.Tools; +#end + +typedef Validatable = { + function validate():Option; +} + +class Validator { + #if macro + static var ARG = 'v'; + var pos:Position; + var IARG:Expr; + function new(pos) { + this.pos = pos; + IARG = macro @:pos(pos) $i{ARG}; + } + + function doCheck(t:Type, e:Expr) { + var ct = t.toComplexType(); + return + macro @:pos (function ($ARG : $ct) ${makeCheck(t)})($e); + } + + function isAtom(s:String) + return switch s { + case 'String', 'Int', 'Bool', 'Float': true; + default: false; + } + + function enforce(type:String) + return + macro @:pos(pos) if (!Std.is($i{ARG}, $i{type})) throw $v{'$type expected'}; + + function rename(e:Expr) + return switch e { + case macro $i{name} if (name == '_'): IARG; + default: e.map(rename); + } + + function makeCheck(t:Type):Expr + return + switch Context.follow(t) { + case TAnonymous(_.get().fields => fields): + + var block:Array = [ + for (f in fields) + if (f.kind.match(FVar(AccNormal, _))) + { + var name = f.name; + var rec = doCheck(f.type, macro @:pos(pos) $IARG.$name); + + if (f.meta.has(':requires')) { + var body = []; + for (m in f.meta.get()) + if (m.name == ':requires') + for (p in m.params) + switch p { + case macro $msg => $p: + body.push(rename( + macro @:pos(pos) if (!$p) throw $msg + )); + default: + Context.error('Should be "" =>" ', p.pos); + } + //{ + //p = rename(p); + //cond = macro @:pos(pos) $p && $cond; + //} + + var t = f.type.toComplexType(); + rec = macro @:pos(pos) { + $rec; + (function($ARG : $t) $b{body})($IARG.$name); + } + } + + if (f.meta.has(':optional')) { + rec = macro @:pos(pos) if (Reflect.hasField($IARG, $v{name}) && $IARG.$name != null) $rec; + } + else + rec = macro @:pos(pos) + if (!Reflect.hasField($IARG, $v{name})) + throw ("missing field " + $v{name}); + else + $rec; + + rec; + } + ]; + + block.unshift( + macro @:pos(pos) if (!Reflect.isObject($IARG)) throw 'object expected' + ); + + macro @:pos(pos) $b{block}; + + case _.toString() => atom if (isAtom(atom)): + + enforce(atom); + + case TInst(_.get().module => 'Array', [p]): + + macro @:pos(pos) { + ${enforce('Array')}; + for ($IARG in $IARG) + ${doCheck(p, IARG)}; + } + + case TAbstract(_.get() => { from: [ { t: t, field: null } ] }, _): + + makeCheck(t); + + case TAbstract(_.get() => a, _) if (a.meta.has(':enum')): + var name = a.module + '.' + a.name; + var options:Array = [ + for (f in a.impl.get().statics.get()) + if (f.kind.match(FVar(_, _))) + macro @:pos(pos) $p{(name+'.'+f.name).split('.')} + ]; + + macro if (!Lambda.has($a { options }, $IARG)) throw 'Invalid value ' + $IARG + ' for ' + $v { a.name }; + + case TAbstract(_.get() => a, _): + + macro @:pos(pos) switch ($IARG : haxelib.Validator.Validatable).validate() { + case Some(e): throw e; + case None: + } + + case TDynamic(k): + var checker = makeCheck(k); + var ct = k.toComplexType(); + macro @:pos(pos) { + if (!Reflect.isObject($i{ARG})) throw 'object expected'; + for (f in Reflect.fields($i{ARG})) { + var $ARG:$ct = Reflect.field($i{ARG}, f); + $checker; + } + } + case v: + throw t.toString(); + } + #end + macro static public function validate(e:Expr) + return + new Validator(e.pos).doCheck(Context.typeof(e), e); +} \ No newline at end of file diff --git a/tools/haxelib/client/Cli.hx b/tools/haxelib/client/Cli.hx new file mode 100644 index 000000000..b0c5fbea5 --- /dev/null +++ b/tools/haxelib/client/Cli.hx @@ -0,0 +1,46 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib.client; + +class Cli { + public static var defaultAnswer:Null; + + public static function ask(question:String):Bool { + if (defaultAnswer != null) + return defaultAnswer; + + while (true) { + Sys.print(question + " [y/n/a] ? "); + try { + switch (Sys.stdin().readLine()) { + case "n": return false; + case "y": return true; + case "a": return defaultAnswer = true; + } + } catch (e:haxe.io.Eof) { + Sys.println("n"); + return false; + } + } + return false; + } +} diff --git a/tools/haxelib/client/ConvertXml.hx b/tools/haxelib/client/ConvertXml.hx new file mode 100644 index 000000000..b71960336 --- /dev/null +++ b/tools/haxelib/client/ConvertXml.hx @@ -0,0 +1,96 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib.client; + +import haxe.Json; + +class ConvertXml { + public static function convert(inXml:String) { + // Set up the default JSON structure + var json = { + "name": "", + "url" : "", + "license": "", + "tags": [], + "description": "", + "version": "0.0.1", + "releasenote": "", + "contributors": [], + "dependencies": {} + }; + + // Parse the XML and set the JSON + var xml = Xml.parse(inXml); + var project = xml.firstChild(); + json.name = project.get("name"); + json.license = project.get("license"); + json.url = project.get("url"); + for (node in project) { + switch (node.nodeType) { + case #if (haxe_ver >= 3.2) Element #else Xml.Element #end: + switch (node.nodeName) { + case "tag": + json.tags.push(node.get("v")); + case "user": + json.contributors.push(node.get("name")); + case "version": + json.version = node.get("name"); + json.releasenote = node.firstChild().toString(); + case "description": + json.description = node.firstChild().toString(); + case "depends": + var name = node.get("name"); + var version = node.get("version"); + if (version == null) version = ""; + Reflect.setField(json.dependencies, name, version); + default: + } + default: + } + } + + return json; + } + + public static function prettyPrint(json:Dynamic, indent="") { + var sb = new StringBuf(); + sb.add("{\n"); + + var firstRun = true; + for (f in Reflect.fields(json)) { + if (!firstRun) sb.add(",\n"); + firstRun = false; + + var value = switch (f) { + case "dependencies": + var d = Reflect.field(json, f); + prettyPrint(d, indent + " "); + default: + Json.stringify(Reflect.field(json, f)); + } + sb.add(indent+' "$f": $value'); + } + + sb.add('\n$indent}'); + return sb.toString(); + } +} diff --git a/tools/haxelib/client/FsUtils.hx b/tools/haxelib/client/FsUtils.hx new file mode 100644 index 000000000..749258360 --- /dev/null +++ b/tools/haxelib/client/FsUtils.hx @@ -0,0 +1,131 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib.client; + +import haxe.io.Path; +import sys.FileSystem; +using StringTools; + +class FsUtils { + static var IS_WINDOWS = (Sys.systemName() == "Windows"); + + //recursively follow symlink + public static function realPath(path:String):String { + var proc = new sys.io.Process('readlink', [path.endsWith("\n") ? path.substr(0, path.length-1) : path]); + var ret = switch (proc.stdout.readAll().toString()) { + case "": //it is not a symlink + path; + case targetPath: + if (targetPath.startsWith("/")) { + realPath(targetPath); + } else { + realPath(new Path(path).dir + "/" + targetPath); + } + } + proc.close(); + return ret; + } + + public static function isSamePath(a:String, b:String):Bool { + a = Path.normalize(a); + b = Path.normalize(b); + if (IS_WINDOWS) { // paths are case-insensitive on Windows + a = a.toLowerCase(); + b = b.toLowerCase(); + } + return a == b; + } + + public static function safeDir(dir:String, checkWritable = false):Bool { + if (FileSystem.exists(dir)) { + if (!FileSystem.isDirectory(dir)) + throw 'A file is preventing $dir to be created'; + if (checkWritable) { + var checkFile = dir+"/haxelib_writecheck.txt"; + try { + sys.io.File.saveContent(checkFile, "This is a temporary file created by Haxelib to check if directory is writable. You can safely delete it!"); + } catch (_:Dynamic) { + throw '$dir exists but is not writeable, chmod it'; + } + FileSystem.deleteFile(checkFile); + } + return false; + } else { + try { + FileSystem.createDirectory(dir); + return true; + } catch (_:Dynamic) { + throw 'You don\'t have enough user rights to create the directory $dir'; + } + } + } + + public static function deleteRec(dir:String):Bool { + if (!FileSystem.exists(dir)) + return false; + for (p in FileSystem.readDirectory(dir)) { + var path = Path.join([dir, p]); + + if (isBrokenSymlink(path)) { + safeDelete(path); + } else if (FileSystem.isDirectory(path)) { + if (!IS_WINDOWS) { + // try to delete it as a file first - in case of path + // being a symlink, it will success + if (!safeDelete(path)) + deleteRec(path); + } else { + deleteRec(path); + } + } else { + safeDelete(path); + } + } + FileSystem.deleteDirectory(dir); + return true; + } + + static function safeDelete(file:String):Bool { + try { + FileSystem.deleteFile(file); + return true; + } catch (e:Dynamic) { + if (IS_WINDOWS) { + try { + Sys.command("attrib", ["-R", file]); + FileSystem.deleteFile(file); + return true; + } catch (_:Dynamic) { + } + } + return false; + } + } + + static function isBrokenSymlink(path:String):Bool { + // TODO: figure out what this method actually does :) + var errors = 0; + try FileSystem.isDirectory(path) catch (error:String) if (error == "std@sys_file_type") errors++; + try FileSystem.fullPath(path) catch (error:String) if (error == "std@file_full_path") errors++; + return errors == 2; + } +} diff --git a/tools/haxelib/client/Main.hx b/tools/haxelib/client/Main.hx new file mode 100644 index 000000000..ab1847ccf --- /dev/null +++ b/tools/haxelib/client/Main.hx @@ -0,0 +1,1607 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib.client; + +import haxe.crypto.Md5; +import haxe.*; +import haxe.io.BytesOutput; +import haxe.io.Path; +import haxe.zip.*; +import sys.io.File; +import sys.FileSystem; +import sys.io.*; +import haxe.ds.Option; +import haxelib.client.Cli.ask; +import haxelib.client.FsUtils.*; +import haxelib.client.Vcs; + +using StringTools; +using Lambda; +using haxelib.Data; + +private enum CommandCategory { + Basic; + Information; + Development; + Miscellaneous; + Deprecated(msg:String); +} + +class SiteProxy extends haxe.remoting.Proxy { +} + +class ProgressOut extends haxe.io.Output { + + var o : haxe.io.Output; + var cur : Int; + var startSize : Int; + var max : Null; + var start : Float; + + public function new(o, currentSize) { + this.o = o; + startSize = currentSize; + cur = currentSize; + start = Timer.stamp(); + } + + function report(n) { + cur += n; + if( max == null ) + Sys.print(cur+" bytes\r"); + else + Sys.print(cur+"/"+max+" ("+Std.int((cur*100.0)/max)+"%)\r"); + } + + public override function writeByte(c) { + o.writeByte(c); + report(1); + } + + public override function writeBytes(s,p,l) { + var r = o.writeBytes(s,p,l); + report(r); + return r; + } + + public override function close() { + super.close(); + o.close(); + var time = Timer.stamp() - start; + var downloadedBytes = cur - startSize; + var speed = (downloadedBytes / time) / 1024; + time = Std.int(time * 10) / 10; + speed = Std.int(speed * 10) / 10; + Sys.print("Download complete : "+downloadedBytes+" bytes in "+time+"s ("+speed+"KB/s)\n"); + } + + public override function prepare(m) { + max = m + startSize; + } + +} + +class ProgressIn extends haxe.io.Input { + + var i : haxe.io.Input; + var pos : Int; + var tot : Int; + + public function new( i, tot ) { + this.i = i; + this.pos = 0; + this.tot = tot; + } + + public override function readByte() { + var c = i.readByte(); + report(1); + return c; + } + + public override function readBytes(buf,pos,len) { + var k = i.readBytes(buf,pos,len); + report(k); + return k; + } + + function report( nbytes : Int ) { + pos += nbytes; + Sys.print( Std.int((pos * 100.0) / tot) + "%\r" ); + } + +} + +class Main { + static inline var HAXELIB_LIBNAME = "haxelib"; + + static var VERSION = SemVer.ofString('3.3.0'); + static var REPNAME = "lib"; + static var REPODIR = ".haxelib"; + static var SERVER = { + host : "lib.haxe.org", + port : 80, + dir : "", + url : "index.n", + apiVersion : "3.0", + }; + static var IS_WINDOWS = (Sys.systemName() == "Windows"); + + var argcur : Int; + var args : Array; + var commands : List<{ name : String, doc : String, f : Void -> Void, net : Bool, cat : CommandCategory }>; + var siteUrl : String; + var site : SiteProxy; + var isHaxelibRun : Bool; + var alreadyUpdatedVcsDependencies:Map = new Map(); + + + function new() { + args = Sys.args(); + isHaxelibRun = (Sys.getEnv("HAXELIB_RUN_NAME") == HAXELIB_LIBNAME); + + if (isHaxelibRun) + Sys.setCwd(args.pop()); + + commands = new List(); + addCommand("install", install, "install a given library, or all libraries from a hxml file", Basic); + addCommand("update", update, "update a single library (if given) or all installed libraries", Basic); + addCommand("remove", remove, "remove a given library/version", Basic, false); + addCommand("list", list, "list all installed libraries", Basic, false); + addCommand("set", set, "set the current version for a library", Basic, false); + + addCommand("search", search, "list libraries matching a word", Information); + addCommand("info", info, "list information on a given library", Information); + addCommand("user", user, "list information on a given user", Information); + addCommand("config", config, "print the repository path", Information, false); + addCommand("path", path, "give paths to libraries", Information, false); + addCommand("version", version, "print the currently used haxelib version", Information, false); + addCommand("help", usage, "display this list of options", Information, false); + + addCommand("submit", submit, "submit or update a library package", Development); + addCommand("register", register, "register a new user", Development); + addCommand("dev", dev, "set the development directory for a given library", Development, false); + //TODO: generate command about VCS by Vcs.getAll() + addCommand("git", vcs.bind(VcsID.Git), "use Git repository as library", Development); + addCommand("hg", vcs.bind(VcsID.Hg), "use Mercurial (hg) repository as library", Development); + + addCommand("setup", setup, "set the haxelib repository path", Miscellaneous, false); + addCommand("newrepo", newRepo, "create a new local repository", Miscellaneous, false); + addCommand("deleterepo", deleteRepo, "delete the local repository", Miscellaneous, false); + addCommand("convertxml", convertXml, "convert haxelib.xml file to haxelib.json", Miscellaneous); + addCommand("run", run, "run the specified library with parameters", Miscellaneous, false); + addCommand("proxy", proxy, "setup the Http proxy", Miscellaneous); + + // deprecated commands + addCommand("local", local, "install the specified package locally", Deprecated("Use `haxelib install ` instead"), false); + addCommand("selfupdate", updateSelf, "update haxelib itself", Deprecated('Use `haxelib --global update $HAXELIB_LIBNAME` instead')); + + initSite(); + } + + function checkUpdate() { + var latest = try site.getLatestVersion(HAXELIB_LIBNAME) catch (_:Dynamic) null; + if (latest != null && latest > VERSION) + print('\nA new version ($latest) of haxelib is available.\nDo `haxelib --global update $HAXELIB_LIBNAME` to get the latest version.\n'); + } + + function initSite() { + siteUrl = "http://" + SERVER.host + ":" + SERVER.port + "/" + SERVER.dir; + var remotingUrl = siteUrl + "api/" + SERVER.apiVersion + "/" + SERVER.url; + site = new SiteProxy(haxe.remoting.HttpConnection.urlConnect(remotingUrl).api); + } + + function param( name, ?passwd ) { + if( args.length > argcur ) + return args[argcur++]; + Sys.print(name+" : "); + if( passwd ) { + var s = new StringBuf(); + do switch Sys.getChar(false) { + case 10, 13: break; + case c: s.addChar(c); + } + while (true); + print(""); + return s.toString(); + } + return Sys.stdin().readLine(); + } + + function paramOpt() { + if( args.length > argcur ) + return args[argcur++]; + return null; + } + + function addCommand( name, f, doc, cat, ?net = true ) { + commands.add({ name : name, doc : doc, f : f, net : net, cat : cat }); + } + + function version() { + print(VERSION); + } + + function usage() { + var cats = []; + var maxLength = 0; + for( c in commands ) { + if (c.name.length > maxLength) maxLength = c.name.length; + if (c.cat.match(Deprecated(_))) continue; + var i = c.cat.getIndex(); + if (cats[i] == null) cats[i] = [c]; + else cats[i].push(c); + } + + print("Haxe Library Manager " + VERSION + " - (c)2006-2016 Haxe Foundation"); + print(" Usage: haxelib [command] [options]"); + + for (cat in cats) { + print(" " + cat[0].cat.getName()); + for (c in cat) { + print(" " + StringTools.rpad(c.name, " ", maxLength) + ": " +c.doc); + } + } + + print(" Available switches"); + for (f in Reflect.fields(ABOUT_SETTINGS)) + print(' --' + f.rpad(' ', maxLength-2) + ": " + Reflect.field(ABOUT_SETTINGS, f)); + } + static var ABOUT_SETTINGS = { + global : "force global repo if a local one exists", + debug : "run in debug mode, imply not --quiet", + quiet : "print less messages, imply not --debug", + flat : "do not use --recursive cloning for git", + always : "answer all questions with yes", + never : "answer all questions with no", + system : "run bundled haxelib version instead of latest update", + } + + var settings: { + debug : Bool, + quiet : Bool, + flat : Bool, + always : Bool, + never : Bool, + global : Bool, + system : Bool, + }; + function process() { + argcur = 0; + var rest = []; + settings = { + debug: false, + quiet: false, + always: false, + never: false, + flat: false, + global: false, + system: false, + }; + + function parseSwitch(s:String) { + return + if (s.startsWith('--')) + Some(s.substr(2)); + else if (s.startsWith('-')) + Some(s.substr(1)); + else + None; + } + + while ( argcur < args.length) { + var a = args[argcur++]; + switch( a ) { + case '-cwd': + var dir = args[argcur++]; + if (dir == null) { + print("Missing directory argument for -cwd"); + Sys.exit(1); + } + try { + Sys.setCwd(dir); + } catch (e:String) { + if (e == "std@set_cwd") { + print("Directory " + dir + " unavailable"); + Sys.exit(1); + } + neko.Lib.rethrow(e); + } + case "-notimeout": + haxe.remoting.HttpConnection.TIMEOUT = 0; + case "-R": + var path = args[argcur++]; + var r = ~/^(http:\/\/)?([^:\/]+)(:[0-9]+)?\/?(.*)$/; + if( !r.match(path) ) + throw "Invalid repository format '"+path+"'"; + SERVER.host = r.matched(2); + if( r.matched(3) != null ) + SERVER.port = Std.parseInt(r.matched(3).substr(1)); + SERVER.dir = r.matched(4); + if (SERVER.dir.length > 0 && !SERVER.dir.endsWith("/")) SERVER.dir += "/"; + initSite(); + case "--debug": + settings.debug = true; + settings.quiet = false; + case "--quiet": + settings.debug = false; + settings.quiet = true; + case parseSwitch(_) => Some(s) if (Reflect.hasField(settings, s)): + //if (!Reflect.hasField(settings, s)) { + //print('unknown switch $a'); + //Sys.exit(1); + //} + Reflect.setField(settings, s, true); + case 'run': + rest = rest.concat(args.slice(argcur - 1)); + break; + default: + rest.push(a); + } + } + + if (!isHaxelibRun && !settings.system) { + var rep = try getGlobalRepository() catch (_:Dynamic) null; + if (rep != null && FileSystem.exists(rep + HAXELIB_LIBNAME)) { + argcur = 0; // send all arguments + doRun(rep, HAXELIB_LIBNAME, null); + return; + } + } + + Cli.defaultAnswer = + switch [settings.always, settings.never] { + case [true, true]: + print('--always and --never are mutually exclusive'); + Sys.exit(1); + null; + case [true, _]: true; + case [_, true]: false; + default: null; + } + + argcur = 0; + args = rest; + + var cmd = args[argcur++]; + if( cmd == null ) { + usage(); + Sys.exit(1); + } + if (cmd == "upgrade") cmd = "update"; // TODO: maybe we should have some alias system + for( c in commands ) + if( c.name == cmd ) { + switch (c.cat) { + case Deprecated(message): + Sys.println('Warning: Command `$cmd` is deprecated and will be removed in future. $message.'); + default: + } + try { + if( c.net ) { + loadProxy(); + checkUpdate(); + } + c.f(); + } catch( e : Dynamic ) { + if( e == "std@host_resolve" ) { + print("Host "+SERVER.host+" was not found"); + print("Please ensure that your internet connection is on"); + print("If you don't have an internet connection or if you are behing a proxy"); + print("please download manually the file from http://lib.haxe.org/files/3.0/"); + print("and run 'haxelib local ' to install the Library."); + print("You can also setup the proxy with 'haxelib proxy'."); + Sys.exit(1); + } + if( e == "Blocked" ) { + print("Http connection timeout. Try running haxelib -notimeout to disable timeout"); + Sys.exit(1); + } + if( e == "std@get_cwd" ) { + print("Error: Current working directory is unavailable"); + Sys.exit(1); + } + if( settings.debug ) + neko.Lib.rethrow(e); + print("Error: " + Std.string(e)); + Sys.exit(1); + } + return; + } + print("Unknown command "+cmd); + usage(); + Sys.exit(1); + } + + inline function createHttpRequest(url:String):Http { + var req = new Http(url); + if (haxe.remoting.HttpConnection.TIMEOUT == 0) + req.cnxTimeout = 0; + return req; + } + + // ---- COMMANDS -------------------- + + function search() { + var word = param("Search word"); + var l = site.search(word); + for( s in l ) + print(s.name); + print(l.length+" libraries found"); + } + + function info() { + var prj = param("Library name"); + var inf = site.infos(prj); + print("Name: "+inf.name); + print("Tags: "+inf.tags.join(", ")); + print("Desc: "+inf.desc); + print("Website: "+inf.website); + print("License: "+inf.license); + print("Owner: "+inf.owner); + print("Version: "+inf.getLatest()); + print("Releases: "); + if( inf.versions.length == 0 ) + print(" (no version released yet)"); + for( v in inf.versions ) + print(" "+v.date+" "+v.name+" : "+v.comments); + } + + function user() { + var uname = param("User name"); + var inf = site.user(uname); + print("Id: "+inf.name); + print("Name: "+inf.fullname); + print("Mail: "+inf.email); + print("Libraries: "); + if( inf.projects.length == 0 ) + print(" (no libraries)"); + for( p in inf.projects ) + print(" "+p); + } + + function register() { + doRegister(param("User")); + print("Registration successful"); + } + + function doRegister(name) { + var email = param("Email"); + var fullname = param("Fullname"); + var pass = param("Password",true); + var pass2 = param("Confirm",true); + if( pass != pass2 ) + throw "Password does not match"; + pass = Md5.encode(pass); + site.register(name,pass,email,fullname); + return pass; + } + + function zipDirectory(root:String):List { + var ret = new List(); + function seek(dir:String) { + for (name in FileSystem.readDirectory(dir)) if (!name.startsWith('.')) { + var full = '$dir/$name'; + if (FileSystem.isDirectory(full)) seek(full); + else { + var blob = File.getBytes(full); + var entry:Entry = { + fileName: full.substr(root.length+1), + fileSize : blob.length, + fileTime : FileSystem.stat(full).mtime, + compressed : false, + dataSize : blob.length, + data : blob, + crc32: haxe.crypto.Crc32.make(blob), + }; + Tools.compress(entry, 9); + ret.push(entry); + } + } + } + seek(root); + return ret; + } + + function submit() { + var file = param("Package"); + + var data, zip; + if (FileSystem.isDirectory(file)) { + zip = zipDirectory(file); + var out = new BytesOutput(); + new Writer(out).write(zip); + data = out.getBytes(); + } else { + data = File.getBytes(file); + zip = Reader.readZip(new haxe.io.BytesInput(data)); + } + + var infos = Data.readInfos(zip,true); + Data.checkClassPath(zip, infos); + + var user:String = infos.contributors[0]; + + if (infos.contributors.length > 1) + do { + print("Which of these users are you: " + infos.contributors); + user = param("User"); + } while ( infos.contributors.indexOf(user) == -1 ); + + var password; + if( site.isNewUser(user) ) { + print("This is your first submission as '"+user+"'"); + print("Please enter the following information for registration"); + password = doRegister(user); + } else { + password = readPassword(user); + } + site.checkDeveloper(infos.name,user); + + // check dependencies validity + for( d in infos.dependencies ) { + var infos = site.infos(d.name); + if( d.version == "" ) + continue; + var found = false; + for( v in infos.versions ) + if( v.name == d.version ) { + found = true; + break; + } + if( !found ) + throw "Library " + d.name + " does not have version " + d.version; + } + + // check if this version already exists + + var sinfos = try site.infos(infos.name) catch( _ : Dynamic ) null; + if( sinfos != null ) + for( v in sinfos.versions ) + if( v.name == infos.version && !ask("You're about to overwrite existing version '"+v.name+"', please confirm") ) + throw "Aborted"; + + // query a submit id that will identify the file + var id = site.getSubmitId(); + + // directly send the file data over Http + var h = createHttpRequest("http://"+SERVER.host+":"+SERVER.port+"/"+SERVER.url); + h.onError = function(e) throw e; + h.onData = print; + h.fileTransfer("file",id,new ProgressIn(new haxe.io.BytesInput(data),data.length),data.length); + print("Sending data.... "); + h.request(true); + + // processing might take some time, make sure we wait + print("Processing file.... "); + if (haxe.remoting.HttpConnection.TIMEOUT != 0) // don't ignore -notimeout + haxe.remoting.HttpConnection.TIMEOUT = 1000; + // ask the server to register the sent file + var msg = site.processSubmit(id,user,password); + print(msg); + } + + function readPassword(user:String, prompt = "Password"):String { + var password = Md5.encode(param(prompt,true)); + var attempts = 5; + while (!site.checkPassword(user, password)) { + print('Invalid password for $user'); + if (--attempts == 0) + throw 'Failed to input correct password'; + password = Md5.encode(param('$prompt ($attempts more attempt${attempts == 1 ? "" : "s"})', true)); + } + return password; + } + + function install() { + var rep = getRepository(); + + var prj = param("Library name or hxml file:"); + + // No library given, install libraries listed in *.hxml in given directory + if( prj == "all") { + installFromAllHxml(rep); + return; + } + + if( sys.FileSystem.exists(prj) && !sys.FileSystem.isDirectory(prj) ) { + // *.hxml provided, install all libraries/versions in this hxml file + if( prj.endsWith(".hxml") ) { + installFromHxml(rep, prj); + return; + } + // *.zip provided, install zip as haxe library + if (prj.endsWith(".zip")) { + doInstallFile(rep, prj, true, true); + return; + } + + if ( prj.endsWith("haxelib.json") ) + { + installFromHaxelibJson( rep, prj); + return; + } + } + + // Name provided that wasn't a local hxml or zip, so try to install it from server + var inf = site.infos(prj); + var reqversion = paramOpt(); + var version = getVersion(inf, reqversion); + doInstall(rep,inf.name,version,version == inf.getLatest()); + } + + function getVersion( inf:ProjectInfos, ?reqversion:String ) { + if( inf.versions.length == 0 ) + throw "The library "+inf.name+" has not yet released a version"; + var version = if( reqversion != null ) reqversion else inf.getLatest(); + var found = false; + for( v in inf.versions ) + if( v.name == version ) { + found = true; + break; + } + if( !found ) + throw "No such version "+version+" for library "+inf.name; + + return version; + } + + function installFromHxml( rep:String, path:String ) { + var targets = [ + '-java ' => 'hxjava', + '-cpp ' => 'hxcpp', + '-cs ' => 'hxcs', + ]; + var libsToInstall = new Map(); + + function processHxml(path) { + var hxml = sys.io.File.getContent(path); + var lines = hxml.split("\n"); + for (l in lines) { + l = l.trim(); + for (target in targets.keys()) + if (l.startsWith(target)) { + var lib = targets[target]; + if (!libsToInstall.exists(lib)) + libsToInstall[lib] = { name: lib, version: null, type:"haxelib", url: null, branch: null, subDir: null } + } + + if (l.startsWith("-lib")) + { + var key = l.substr(5).trim(); + var parts = ~/:/.split(key); + var libName = parts[0]; + var libVersion:String = null; + var branch:String = null; + var url:String = null; + var subDir:String = null; + var type:String; + + if ( parts.length > 1 ) + { + if ( parts[1].startsWith("git:") ) + { + + type = "git"; + var urlParts = parts[1].substr(4).split("#"); + url = urlParts[0]; + branch = urlParts.length > 1 ? urlParts[1] : null; + } + else + { + type = "haxelib"; + libVersion = parts[1]; + } + } + else + { + type = "haxelib"; + } + + switch libsToInstall[key] { + case null, { version: null } : + libsToInstall.set(key, { name:libName, version:libVersion, type: type, url: url, subDir: subDir, branch: branch } ); + default: + } + } + + if (l.endsWith(".hxml")) + processHxml(l); + } + } + processHxml(path); + + if (libsToInstall.empty()) + return; + + // Check the version numbers are all good + // TODO: can we collapse this into a single API call? It's getting too slow otherwise. + print("Loading info about the required libraries"); + for (l in libsToInstall) + { + if ( l.type == "git" ) + { + // Do not check git repository infos + continue; + } + var inf = site.infos(l.name); + l.version = getVersion(inf, l.version); + } + + // Print a list with all the info + print("Haxelib is going to install these libraries:"); + for (l in libsToInstall) { + var vString = (l.version == null) ? "" : " - " + l.version; + print(" " + l.name + vString); + } + + // Install if they confirm + if (ask("Continue?")) { + for (l in libsToInstall) { + if ( l.type == "haxelib" ) + doInstall(rep, l.name, l.version, true); + else if ( l.type == "git" ) + useVcs(VcsID.Git, function(vcs) doVcsInstall(rep, vcs, l.name, l.url, l.branch, l.subDir, l.version)); + else if ( l.type == "hg" ) + useVcs(VcsID.Hg, function(vcs) doVcsInstall(rep, vcs, l.name, l.url, l.branch, l.subDir, l.version)); + } + } + } + + function installFromHaxelibJson( rep:String, path:String ) + { + doInstallDependencies(rep, Data.readData(File.getContent(path), false).dependencies); + } + + function installFromAllHxml(rep:String) { + var cwd = Sys.getCwd(); + var hxmlFiles = sys.FileSystem.readDirectory(cwd).filter(function (f) return f.endsWith(".hxml")); + if (hxmlFiles.length > 0) { + for (file in hxmlFiles) { + print('Installing all libraries from $file:'); + installFromHxml(rep, cwd + file); + } + } else { + print("No hxml files found in the current directory."); + } + } + + function doInstall( rep, project, version, setcurrent ) { + // check if exists already + if( FileSystem.exists(rep+Data.safe(project)+"/"+Data.safe(version)) ) { + print("You already have "+project+" version "+version+" installed"); + setCurrent(rep,project,version,true); + return; + } + + // download to temporary file + var filename = Data.fileName(project,version); + var filepath = rep+filename; + var out = try File.append(filepath,true) catch (e:Dynamic) throw 'Failed to write to $filepath: $e'; + out.seek(0, SeekEnd); + + var h = createHttpRequest(siteUrl+Data.REPOSITORY+"/"+filename); + + var currentSize = out.tell(); + if (currentSize > 0) + h.addHeader("range", "bytes="+currentSize + "-"); + + var progress = new ProgressOut(out, currentSize); + + var has416Status = false; + h.onStatus = function(status) { + // 416 Requested Range Not Satisfiable, which means that we probably have a fully downloaded file already + if (status == 416) has416Status = true; + }; + h.onError = function(e) { + progress.close(); + + // if we reached onError, because of 416 status code, it's probably okay and we should try unzipping the file + if (!has416Status) { + FileSystem.deleteFile(filepath); + throw e; + } + }; + print("Downloading "+filename+"..."); + h.customRequest(false,progress); + + doInstallFile(rep,filepath, setcurrent); + try { + site.postInstall(project, version); + } catch (e:Dynamic) {} + } + + function doInstallFile(rep,filepath,setcurrent,nodelete = false) { + // read zip content + var f = File.read(filepath,true); + var zip = try { + Reader.readZip(f); + } catch (e:Dynamic) { + f.close(); + // file is corrupted, remove it + if (!nodelete) + FileSystem.deleteFile(filepath); + neko.Lib.rethrow(e); + throw e; + } + f.close(); + var infos = Data.readInfos(zip,false); + print('Installing ${infos.name}...'); + // create directories + var pdir = rep + Data.safe(infos.name); + safeDir(pdir); + pdir += "/"; + var target = pdir + Data.safe(infos.version); + safeDir(target); + target += "/"; + + // locate haxelib.json base path + var basepath = Data.locateBasePath(zip); + + // unzip content + var entries = [for (entry in zip) if (entry.fileName.startsWith(basepath)) entry]; + var total = entries.length; + for (i in 0...total) { + var zipfile = entries[i]; + var n = zipfile.fileName; + // remove basepath + n = n.substr(basepath.length,n.length-basepath.length); + if( n.charAt(0) == "/" || n.charAt(0) == "\\" || n.split("..").length > 1 ) + throw "Invalid filename : "+n; + + if (!settings.debug) { + var percent = Std.int((i / total) * 100); + Sys.print('${i + 1}/$total ($percent%)\r'); + } + + var dirs = ~/[\/\\]/g.split(n); + var path = ""; + var file = dirs.pop(); + for( d in dirs ) { + path += d; + safeDir(target+path); + path += "/"; + } + if( file == "" ) { + if( path != "" && settings.debug ) print(" Created "+path); + continue; // was just a directory + } + path += file; + if (settings.debug) + print(" Install "+path); + var data = Reader.unzip(zipfile); + File.saveBytes(target+path,data); + } + + // set current version + if( setcurrent || !FileSystem.exists(pdir+".current") ) { + File.saveContent(pdir + ".current", infos.version); + print(" Current version is now "+infos.version); + } + + // end + if( !nodelete ) + FileSystem.deleteFile(filepath); + print("Done"); + + // process dependencies + doInstallDependencies(rep, infos.dependencies); + + return infos; + } + + function doInstallDependencies( rep:String, dependencies:Array ) { + for( d in dependencies ) { + if( d.version == "" ) { + var pdir = rep + Data.safe(d.name); + var dev = try getDev(pdir) catch (_:Dynamic) null; + + if (dev != null) { // no version specified and dev set, no need to install dependency + continue; + } + } + + if( d.version == "" && d.type == DependencyType.Haxelib ) + d.version = site.getLatestVersion(d.name); + print("Installing dependency "+d.name+" "+d.version); + + switch d.type { + case Haxelib: + doInstall(rep, d.name, d.version, false); + case Git: + useVcs(VcsID.Git, function(vcs) doVcsInstall(rep, vcs, d.name, d.url, d.branch, d.subDir, d.version)); + case Mercurial: + useVcs(VcsID.Hg, function(vcs) doVcsInstall(rep, vcs, d.name, d.url, d.branch, d.subDir, d.version)); + } + } + } + + static public function getConfigFile():String { + var home = null; + if (IS_WINDOWS) { + home = Sys.getEnv("USERPROFILE"); + if (home == null) { + var drive = Sys.getEnv("HOMEDRIVE"); + var path = Sys.getEnv("HOMEPATH"); + if (drive != null && path != null) + home = drive + path; + } + if (home == null) + throw "Could not determine home path. Please ensure that USERPROFILE or HOMEDRIVE+HOMEPATH environment variables are set."; + } else { + home = Sys.getEnv("HOME"); + if (home == null) + throw "Could not determine home path. Please ensure that HOME environment variable is set."; + } + return Path.addTrailingSlash(home) + ".haxelib"; + } + + function getGlobalRepositoryPath(create = false):String { + // first check the env var + var rep = Sys.getEnv("HAXELIB_PATH"); + if (rep != null) + return rep.trim(); + + // try to read from user config + rep = try File.getContent(getConfigFile()).trim() catch (_:Dynamic) null; + if (rep != null) + return rep; + + if (!IS_WINDOWS) { + // on unixes, try to read system-wide config + rep = try File.getContent("/etc/.haxelib").trim() catch (_:Dynamic) null; + if (rep == null) + throw "This is the first time you are runing haxelib. Please run `haxelib setup` first"; + } else { + // on windows, try to use haxe installation path + rep = getWindowsDefaultGlobalRepositoryPath(); + if (create) + try safeDir(rep) catch(e:Dynamic) throw 'Error accessing Haxelib repository: $e'; + } + + return rep; + } + + // The Windows haxe installer will setup %HAXEPATH%. We will default haxelib repo to %HAXEPATH%/lib. + // When there is no %HAXEPATH%, we will use a "haxelib" directory next to the config file, ".haxelib". + function getWindowsDefaultGlobalRepositoryPath():String { + var haxepath = Sys.getEnv("HAXEPATH"); + if (haxepath != null) + return Path.addTrailingSlash(haxepath.trim()) + REPNAME; + else + return Path.join([Path.directory(getConfigFile()), "haxelib"]); + } + + function getSuggestedGlobalRepositoryPath():String { + if (IS_WINDOWS) + return getWindowsDefaultGlobalRepositoryPath(); + + return if (FileSystem.exists("/usr/share/haxe")) // for Debian + '/usr/share/haxe/$REPNAME' + else if (Sys.systemName() == "Mac") // for newer OSX, where /usr/lib is not writable + '/usr/local/lib/haxe/$REPNAME' + else + '/usr/lib/haxe/$REPNAME'; // for other unixes + } + + function getRepository():String { + if (!settings.global && FileSystem.exists(REPODIR) && FileSystem.isDirectory(REPODIR)) + return Path.addTrailingSlash(FileSystem.fullPath(REPODIR)); + else + return getGlobalRepository(); + } + + function getGlobalRepository():String { + var rep = getGlobalRepositoryPath(true); + if (!FileSystem.exists(rep)) + throw "haxelib Repository " + rep + " does not exist. Please run `haxelib setup` again."; + else if (!FileSystem.isDirectory(rep)) + throw "haxelib Repository " + rep + " exists, but is a file, not a directory. Please remove it and run `haxelib setup` again."; + return Path.addTrailingSlash(rep); + } + + function setup() { + var rep = try getGlobalRepositoryPath() catch (_:Dynamic) null; + + var configFile = getConfigFile(); + + if (args.length <= argcur) { + if (rep == null) + rep = getSuggestedGlobalRepositoryPath(); + print("Please enter haxelib repository path with write access"); + print("Hit enter for default (" + rep + ")"); + } + + var line = param("Path"); + if (line != "") + rep = line; + + rep = try FileSystem.fullPath(rep) catch (_:Dynamic) rep; + + if (isSamePath(rep, configFile)) + throw "Can't use "+rep+" because it is reserved for config file"; + + safeDir(rep); + File.saveContent(configFile, rep); + + print("haxelib repository is now " + rep); + } + + function config() { + print(getRepository()); + } + + function getCurrent( dir ) { + return (FileSystem.exists(dir+"/.dev")) ? "dev" : File.getContent(dir + "/.current").trim(); + } + + function getDev( dir ) { + return File.getContent(dir + "/.dev").trim(); + } + + function matchVersion( version, other ) { + if (version == "" || version == null) + return true; + if (other == "" || other == null) + return false; + var filter = version.replace(".","\\.").replace("*",".*"); + return new EReg("^"+filter,"i").match(other); + } + + function getVersionDir( version, dev, dir ) { + if ( dev != null ) { + var json = try File.getContent(dev+"/"+Data.JSON) catch( e : Dynamic ) null; + var inf = Data.readData(json, false); + if ( version == "dev" || matchVersion(version, inf.version) ) { + return dev; + } + } + var matches = []; + for( v in FileSystem.readDirectory(dir) ) { + if( v.charAt(0) == "." ) + continue; + v = Data.unsafe(v); + var semver = try SemVer.ofString(v) catch (_:Dynamic) null; + if (semver != null && matchVersion(version, semver)) + matches.push(semver); + } + var best = null; + for( match in matches ) { + if (best == null || match > best) { + best = match; + } + } + return if (best != null) dir + "/" + Data.safe(best) else null; + } + + function list() { + var rep = getRepository(); + var folders = FileSystem.readDirectory(rep); + var filter = paramOpt(); + if ( filter != null ) + folders = folders.filter( function (f) return f.toLowerCase().indexOf(filter.toLowerCase()) > -1 ); + var all = []; + for( p in folders ) { + if( p.charAt(0) == "." ) + continue; + + var current = try getCurrent(rep + p) catch(e:Dynamic) continue; + var dev = try getDev(rep + p) catch( e : Dynamic ) null; + + var semvers = []; + var others = []; + for( v in FileSystem.readDirectory(rep+p) ) { + if( v.charAt(0) == "." ) + continue; + v = Data.unsafe(v); + var semver = try SemVer.ofString(v) catch (_:Dynamic) null; + if (semver != null) + semvers.push(semver); + else + others.push(v); + } + + if (semvers.length > 0) + semvers.sort(SemVer.compare); + + var versions = []; + for (v in semvers) + versions.push((v : String)); + for (v in others) + versions.push(v); + + if (dev == null) { + for (i in 0...versions.length) { + var v = versions[i]; + if (v == current) + versions[i] = '[$v]'; + } + } else { + versions.push("[dev:"+dev+"]"); + } + + all.push(Data.unsafe(p) + ": "+versions.join(" ")); + } + all.sort(function(s1, s2) return Reflect.compare(s1.toLowerCase(), s2.toLowerCase())); + for (p in all) { + print(p); + } + } + + function update() { + var rep = getRepository(); + + var prj = paramOpt(); + if (prj != null) { + prj = projectNameToDir(rep, prj); // get project name in proper case + if (!updateByName(rep, prj)) + print(prj + " is up to date"); + return; + } + + var state = { rep : rep, prompt : true, updated : false }; + for( p in FileSystem.readDirectory(state.rep) ) { + if( p.charAt(0) == "." || !FileSystem.isDirectory(state.rep+"/"+p) ) + continue; + var p = Data.unsafe(p); + print("Checking " + p); + try { + doUpdate(p, state); + } catch (e:VcsError) { + if (!e.match(VcsUnavailable(_))) + neko.Lib.rethrow(e); + } + } + if( state.updated ) + print("Done"); + else + print("All libraries are up-to-date"); + } + + function projectNameToDir( rep:String, project:String ) { + var p = project.toLowerCase(); + var l = FileSystem.readDirectory(rep).filter(function (dir) return dir.toLowerCase() == p); + + switch (l) { + case []: return project; + case [dir]: return Data.unsafe(dir); + case _: throw "Several name case for library " + project; + } + } + + function updateByName(rep:String, prj:String) { + var state = { rep : rep, prompt : false, updated : false }; + doUpdate(prj,state); + return state.updated; + } + + function doUpdate( p : String, state : { updated : Bool, rep : String, prompt : Bool } ) { + var pdir = state.rep + Data.safe(p); + + var vcs = Vcs.getVcsForDevLib(pdir, settings); + if(vcs != null) { + if(!vcs.available) + throw VcsError.VcsUnavailable(vcs); + + var oldCwd = Sys.getCwd(); + Sys.setCwd(pdir + "/" + vcs.directory); + var success = vcs.update(p); + + state.updated = success; + Sys.setCwd(oldCwd); + } else { + var latest = try site.getLatestVersion(p) catch( e : Dynamic ) { Sys.println(e); return; }; + + if( !FileSystem.exists(pdir+"/"+Data.safe(latest)) ) { + if( state.prompt ) { + if (!ask("Update "+p+" to "+latest)) + return; + } + doInstall(state.rep, p, latest,true); + state.updated = true; + } else + setCurrent(state.rep, p, latest, true); + } + } + + function remove() { + var rep = getRepository(); + var prj = param("Library"); + var version = paramOpt(); + var pdir = rep + Data.safe(prj); + if( version == null ) { + if( !FileSystem.exists(pdir) ) + throw "Library "+prj+" is not installed"; + + if (prj == HAXELIB_LIBNAME && isHaxelibRun) { + print('Error: Removing "$HAXELIB_LIBNAME" requires the --system flag'); + Sys.exit(1); + } + + deleteRec(pdir); + print("Library "+prj+" removed"); + return; + } + + var vdir = pdir + "/" + Data.safe(version); + if( !FileSystem.exists(vdir) ) + throw "Library "+prj+" does not have version "+version+" installed"; + + var cur = File.getContent(pdir + "/.current").trim(); // set version regardless of dev + if( cur == version ) + throw "Can't remove current version of library "+prj; + var dev = try getDev(pdir) catch (_:Dynamic) null; // dev is checked here + if( dev == vdir ) + throw "Can't remove dev version of library "+prj; + deleteRec(vdir); + print("Library "+prj+" version "+version+" removed"); + } + + function set() { + setCurrent(getRepository(), param("Library"), param("Version"), false); + } + + function setCurrent( rep : String, prj : String, version : String, doAsk : Bool ) { + var pdir = rep + Data.safe(prj); + var vdir = pdir + "/" + Data.safe(version); + if( !FileSystem.exists(vdir) ){ + print("Library "+prj+" version "+version+" is not installed"); + if(ask("Would you like to install it?")) + doInstall(rep, prj, version, true); + return; + } + if( File.getContent(pdir + "/.current").trim() == version ) + return; + if( doAsk && !ask("Set "+prj+" to version "+version) ) + return; + File.saveContent(pdir+"/.current",version); + print("Library "+prj+" current version is now "+version); + } + + function checkRec( rep : String, prj : String, version : String, l : List<{ project : String, version : String, dir : String, info : Infos }> ) { + var pdir = rep + Data.safe(prj); + if( !FileSystem.exists(pdir) ) + throw "Library "+prj+" is not installed : run 'haxelib install "+prj+"'"; + var version = if( version != null ) version else getCurrent(pdir); + + var dev = try getDev(pdir) catch (_:Dynamic) null; + var vdir = try getVersionDir(version,dev,pdir) catch (_:Dynamic) null; + + if( vdir != null && !FileSystem.exists(vdir) ) + throw "Library "+prj+" version "+version+" is not installed"; + + for( p in l ) + if( p.project == prj ) { + if( p.version == version ) + return; + throw "Library "+prj+" has two version included "+version+" and "+p.version; + } + var json = try File.getContent(vdir+"/"+Data.JSON) catch( e : Dynamic ) null; + var inf = Data.readData(json,false); + l.add({ project : prj, version : version, dir : Path.addTrailingSlash(vdir), info: inf }); + for( d in inf.dependencies ) + if( !Lambda.exists(l, function(e) return e.project == d.name) ) + checkRec(rep,d.name,if( d.version == "" ) null else d.version,l); + } + + function path() { + var rep = getRepository(); + var list = new List(); + while( argcur < args.length ) { + var a = args[argcur++].split(":"); + checkRec(rep, a[0],a[1],list); + } + for( d in list ) { + var ndir = d.dir + "ndll"; + if (FileSystem.exists(ndir)) + Sys.println('-L $ndir/'); + + try { + var f = File.getContent(d.dir + "extraParams.hxml"); + Sys.println(f.trim()); + } catch(_:Dynamic) {} + + var dir = d.dir; + if (d.info.classPath != "") { + var cp = d.info.classPath; + dir = Path.addTrailingSlash( d.dir + cp ); + } + Sys.println(dir); + + Sys.println("-D " + d.project + "="+d.info.version); + } + } + + function dev() { + var rep = getRepository(); + var project = param("Library"); + var dir = paramOpt(); + var proj = rep + Data.safe(project); + if( !FileSystem.exists(proj) ) { + FileSystem.createDirectory(proj); + } + var devfile = proj+"/.dev"; + if( dir == null ) { + if( FileSystem.exists(devfile) ) + FileSystem.deleteFile(devfile); + print("Development directory disabled"); + } + else { + while ( dir.endsWith("/") || dir.endsWith("\\") ) { + dir = dir.substr(0,-1); + } + if (!FileSystem.exists(dir)) { + print('Directory $dir does not exist'); + } else { + dir = FileSystem.fullPath(dir); + try { + File.saveContent(devfile, dir); + print("Development directory set to "+dir); + } + catch (e:Dynamic) { + print('Could not write to $devfile'); + } + } + + } + } + + + function removeExistingDevLib(proj:String):Void { + //TODO: ask if existing repo have changes. + + // find existing repo: + var vcs = Vcs.getVcsForDevLib(proj, settings); + // remove existing repos: + while(vcs != null) { + deleteRec(proj + "/" + vcs.directory); + vcs = Vcs.getVcsForDevLib(proj, settings); + } + } + + inline function useVcs(id:VcsID, fn:Vcs->Void):Void { + // Prepare check vcs.available: + var vcs = Vcs.get(id, settings); + if(vcs == null || !vcs.available) + throw 'Could not use $id, please make sure it is installed and available in your PATH.'; + return fn(vcs); + } + + function vcs(id:VcsID) { + var rep = getRepository(); + useVcs(id, function(vcs) doVcsInstall(rep, vcs, param("Library name"), param(vcs.name + " path"), paramOpt(), paramOpt(), paramOpt())); + } + + function doVcsInstall(rep:String, vcs:Vcs, libName:String, url:String, branch:String, subDir:String, version:String) { + + var proj = rep + Data.safe(libName); + + var libPath = proj + "/" + vcs.directory; + + var jsonPath = libPath + "/haxelib.json"; + + if ( FileSystem.exists(proj + "/" + Data.safe(vcs.directory)) ) { + print("You already have "+libName+" version "+vcs.directory+" installed."); + + var wasUpdated = this.alreadyUpdatedVcsDependencies.exists(libName); + var currentBranch = if (wasUpdated) this.alreadyUpdatedVcsDependencies.get(libName) else null; + + if (branch != null && (!wasUpdated || (wasUpdated && currentBranch != branch)) + && ask("Overwrite branch: " + (currentBranch == null?"":"\"" + currentBranch + "\"") + " with \"" + branch + "\"")) + { + deleteRec(libPath); + this.alreadyUpdatedVcsDependencies.set(libName, branch); + } + else + { + if (!wasUpdated) + { + print("Updating " + libName+" version " + vcs.directory + " ..."); + this.alreadyUpdatedVcsDependencies.set(libName, branch); + updateByName(rep, libName); + setCurrent(rep, libName, vcs.directory, true); + + if(FileSystem.exists(jsonPath)) + doInstallDependencies(rep, Data.readData(File.getContent(jsonPath), false).dependencies); + } + return; + } + } + + print("Installing " +libName + " from " +url + ( branch != null ? " branch: " + branch : "" )); + + try { + vcs.clone(libPath, url, branch, version); + } catch(error:VcsError) { + deleteRec(libPath); + var message = switch(error) { + case VcsUnavailable(vcs): + 'Could not use ${vcs.executable}, please make sure it is installed and available in your PATH.'; + case CantCloneRepo(vcs, repo, stderr): + 'Could not clone ${vcs.name} repository' + (stderr != null ? ":\n" + stderr : "."); + case CantCheckoutBranch(vcs, branch, stderr): + 'Could not checkout branch, tag or path "$branch": ' + stderr; + case CantCheckoutVersion(vcs, version, stderr): + 'Could not checkout tag "$version": ' + stderr; + }; + throw message; + } + + // finish it! + if (subDir != null) { + libPath += "/" + subDir; + File.saveContent(proj + "/.dev", libPath); + print("Development directory set to "+libPath); + } else { + File.saveContent(proj + "/.current", vcs.directory); + print("Library "+libName+" current version is now "+vcs.directory); + } + + this.alreadyUpdatedVcsDependencies.set(libName, branch); + + if(FileSystem.exists(jsonPath)) + doInstallDependencies(rep, Data.readData(File.getContent(jsonPath), false).dependencies); + } + + + function run() { + var rep = getRepository(); + var project = param("Library"); + var temp = project.split(":"); + doRun(rep, temp[0], temp[1]); + } + + function doRun( rep:String, project:String, version:String ) { + var pdir = rep + Data.safe(project); + if( !FileSystem.exists(pdir) ) + throw "Library "+project+" is not installed"; + pdir += "/"; + if (version == null) + version = getCurrent(pdir); + var dev = try getDev(pdir) catch ( e : Dynamic ) null; + var vdir = dev != null ? dev : pdir + Data.safe(version); + + var infos = + try + Data.readData(File.getContent(vdir + '/haxelib.json'), false) + catch (e:Dynamic) + throw 'Error parsing haxelib.json for $project@$version: $e'; + + args.push(Sys.getCwd()); + Sys.setCwd(vdir); + + var callArgs = + if (infos.main == null) { + if( !FileSystem.exists('$vdir/run.n') ) + throw 'Library $project version $version does not have a run script'; + ["neko", vdir + "/run.n"]; + } else { + var deps = infos.dependencies.toArray(); + deps.push( { name: project, version: DependencyVersion.DEFAULT } ); + var args = []; + for (d in deps) { + args.push('-lib'); + args.push(d.name + if (d.version == '') '' else ':${d.version}'); + } + args.unshift('haxe'); + args.push('--run'); + args.push(infos.main); + args; + } + + for (i in argcur...args.length) + callArgs.push(args[i]); + + Sys.putEnv("HAXELIB_RUN", "1"); + Sys.putEnv("HAXELIB_RUN_NAME", project); + var cmd = callArgs.shift(); + Sys.exit(Sys.command(cmd, callArgs)); + } + + function proxy() { + var rep = getRepository(); + var host = param("Proxy host"); + if( host == "" ) { + if( FileSystem.exists(rep + "/.proxy") ) { + FileSystem.deleteFile(rep + "/.proxy"); + print("Proxy disabled"); + } else + print("No proxy specified"); + return; + } + var port = Std.parseInt(param("Proxy port")); + var authName = param("Proxy user login"); + var authPass = authName == "" ? "" : param("Proxy user pass"); + var proxy = { + host : host, + port : port, + auth : authName == "" ? null : { user : authName, pass : authPass }, + }; + Http.PROXY = proxy; + print("Testing proxy..."); + try Http.requestUrl("http://www.google.com") catch( e : Dynamic ) { + print("Proxy connection failed"); + return; + } + File.saveContent(rep + "/.proxy", haxe.Serializer.run(proxy)); + print("Proxy setup done"); + } + + function loadProxy() { + var rep = getRepository(); + try Http.PROXY = haxe.Unserializer.run(File.getContent(rep + "/.proxy")) catch( e : Dynamic ) { }; + } + + function convertXml() { + var cwd = Sys.getCwd(); + var xmlFile = cwd + "haxelib.xml"; + var jsonFile = cwd + "haxelib.json"; + + if (!FileSystem.exists(xmlFile)) { + print('No `haxelib.xml` file was found in the current directory.'); + Sys.exit(0); + } + + var xmlString = File.getContent(xmlFile); + var json = ConvertXml.convert(xmlString); + var jsonString = ConvertXml.prettyPrint(json); + + File.saveContent(jsonFile, jsonString); + print('Saved to $jsonFile'); + } + + function newRepo() { + var path = #if (haxe_ver >= 3.2) FileSystem.absolutePath(REPODIR) #else REPODIR #end; + var created = FsUtils.safeDir(path, true); + if (created) + print('Local repository created ($path)'); + else + print('Local repository already exists ($path)'); + } + + function deleteRepo() { + var path = #if (haxe_ver >= 3.2) FileSystem.absolutePath(REPODIR) #else REPODIR #end; + var deleted = FsUtils.deleteRec(path); + if (deleted) + print('Local repository deleted ($path)'); + else + print('No local repository found ($path)'); + } + + // ---------------------------------- + + inline function print(str) + Sys.println(str); + + static function main() { + new Main().process(); + } + + + // deprecated commands + function local() { + doInstallFile(getRepository(), param("Package"), true, true); + } + + function updateSelf() { + updateByName(getGlobalRepository(), HAXELIB_LIBNAME); + } +} diff --git a/tools/haxelib/client/Vcs.hx b/tools/haxelib/client/Vcs.hx new file mode 100644 index 000000000..a24c1e3db --- /dev/null +++ b/tools/haxelib/client/Vcs.hx @@ -0,0 +1,378 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib.client; + +import sys.FileSystem; +using haxelib.client.Vcs; + +interface IVcs { + var name(default, null):String; + var directory(default, null):String; + var executable(default, null):String; + var available(get, null):Bool; + var settings(default, null):Settings; + + /** + Clone repo into `libPath`. + **/ + function clone(libPath:String, vcsPath:String, ?branch:String, ?version:String):Void; + + /** + Update to HEAD repo contains in CWD or CWD/`Vcs.directory`. + CWD must be like "...haxelib-repo/lib/git" for Git. + Returns `true` if update successful. + **/ + function update(libName:String):Bool; +} + + +@:enum abstract VcsID(String) to String { + var Hg = "hg"; + var Git = "git"; +} + +enum VcsError { + VcsUnavailable(vcs:Vcs); + CantCloneRepo(vcs:Vcs, repo:String, ?stderr:String); + CantCheckoutBranch(vcs:Vcs, branch:String, stderr:String); + CantCheckoutVersion(vcs:Vcs, version:String, stderr:String); +} + + +typedef Settings = { + @:optional var flat:Bool; + @:optional var debug:Bool; + @:optional var quiet:Bool; +} + + +class Vcs implements IVcs { + static var reg:Map; + + public var name(default, null):String; + public var directory(default, null):String; + public var executable(default, null):String; + public var settings(default, null):Settings; + + public var available(get, null):Bool; + + var availabilityChecked = false; + var executableSearched = false; + + public static function initialize(settings:Settings) { + if (reg == null) { + reg = [ + VcsID.Git => new Git(settings), + VcsID.Hg => new Mercurial(settings) + ]; + } else { + if (reg.get(VcsID.Git) == null) + reg.set(VcsID.Git, new Git(settings)); + if (reg.get(VcsID.Hg) == null) + reg.set(VcsID.Hg, new Mercurial(settings)); + } + } + + + function new(executable:String, directory:String, name:String, settings:Settings) { + this.name = name; + this.directory = directory; + this.executable = executable; + this.settings = { + flat: settings.flat != null ? settings.flat : false, + debug: settings.debug != null ? settings.debug : false, + quiet: settings.quiet != null ? settings.quiet : false + } + + if (settings.debug) { + this.settings.quiet = false; + } + } + + + public static function get(id:VcsID, settings:Settings):Null { + initialize(settings); + return reg.get(id); + } + + static function set(id:VcsID, vcs:Vcs, settings:Settings, ?rewrite:Bool):Void { + initialize(settings); + var existing = reg.get(id) != null; + if (!existing || rewrite) + reg.set(id, vcs); + } + + public static function getVcsForDevLib(libPath:String, settings:Settings):Null { + initialize(settings); + for (k in reg.keys()) { + if (FileSystem.exists(libPath + "/" + k) && FileSystem.isDirectory(libPath + "/" + k)) + return reg.get(k); + } + return null; + } + + function sure(commandResult:{code:Int, out:String}):Void { + switch (commandResult) { + case {code: 0}: //pass + case {code: code, out:out}: + if (!settings.debug) + Sys.stderr().writeString(out); + Sys.exit(code); + } + } + + function command(cmd:String, args:Array):{ + code: Int, + out: String + } { + var p = try { + new sys.io.Process(cmd, args); + } catch(e:Dynamic) { + return { + code: -1, + out: Std.string(e) + } + } + var out = p.stdout.readAll().toString(); + var err = p.stderr.readAll().toString(); + if (settings.debug && out != "") + Sys.println(out); + if (settings.debug && err != "") + Sys.stderr().writeString(err); + var code = p.exitCode(); + var ret = { + code: code, + out: code == 0 ? out : err + }; + p.close(); + return ret; + } + + function searchExecutable():Void { + executableSearched = true; + } + + function checkExecutable():Bool { + available = (executable != null) && try command(executable, []).code == 0 catch(_:Dynamic) false; + availabilityChecked = true; + + if (!available && !executableSearched) + searchExecutable(); + + return available; + } + + @:final function get_available():Bool { + if (!availabilityChecked) + checkExecutable(); + return available; + } + + public function clone(libPath:String, vcsPath:String, ?branch:String, ?version:String):Void { + throw "This method must be overriden."; + } + + public function update(libName:String):Bool { + throw "This method must be overriden."; + } +} + + +class Git extends Vcs { + + public function new(settings:Settings) + super("git", "git", "Git", settings); + + override function checkExecutable():Bool { + // with `help` cmd because without any cmd `git` can return exit-code = 1. + available = (executable != null) && try command(executable, ["help"]).code == 0 catch(_:Dynamic) false; + availabilityChecked = true; + + if (!available && !executableSearched) + searchExecutable(); + + return available; + } + + override function searchExecutable():Void { + super.searchExecutable(); + + if (available) + return; + + // if we have already msys git/cmd in our PATH + var match = ~/(.*)git([\\|\/])cmd$/; + for (path in Sys.getEnv("PATH").split(";")) { + if (match.match(path.toLowerCase())) { + var newPath = match.matched(1) + executable + match.matched(2) + "bin"; + Sys.putEnv("PATH", Sys.getEnv("PATH") + ";" + newPath); + } + } + + if (checkExecutable()) + return; + + // look at a few default paths + for (path in ["C:\\Program Files (x86)\\Git\\bin", "C:\\Progra~1\\Git\\bin"]) { + if (FileSystem.exists(path)) { + Sys.putEnv("PATH", Sys.getEnv("PATH") + ";" + path); + if (checkExecutable()) + return; + } + } + } + + override public function update(libName:String):Bool { + if ( + command(executable, ["diff", "--exit-code"]).code != 0 + || + command(executable, ["diff", "--cached", "--exit-code"]).code != 0 + ) { + if (Cli.ask("Reset changes to " + libName + " " + name + " repo so we can pull latest version?")) { + sure(command(executable, ["reset", "--hard"])); + } else { + if (!settings.quiet) + Sys.println(name + " repo left untouched"); + return false; + } + } + + var code = command(executable, ["pull"]).code; + // But if before we pulled specified branch/tag/rev => then possibly currently we haxe "HEAD detached at ..". + if (code != 0) { + // get parent-branch: + var branch = command(executable, ["show-branch"]).out; + var regx = ~/\[([^]]*)\]/; + if (regx.match(branch)) + branch = regx.matched(1); + + sure(command(executable, ["checkout", branch, "--force"])); + sure(command(executable, ["pull"])); + } + return true; + } + + override public function clone(libPath:String, url:String, ?branch:String, ?version:String):Void { + var oldCwd = Sys.getCwd(); + + var vcsArgs = ["clone", url, libPath]; + + if (settings == null || !settings.flat) + vcsArgs.push('--recursive'); + + //TODO: move to Vcs.run(vcsArgs) + //TODO: use settings.quiet + if (command(executable, vcsArgs).code != 0) + throw VcsError.CantCloneRepo(this, url/*, ret.out*/); + + + Sys.setCwd(libPath); + + if (version != null && version != "") { + var ret = command(executable, ["checkout", "tags/" + version]); + if (ret.code != 0) + throw VcsError.CantCheckoutVersion(this, version, ret.out); + } else if (branch != null) { + var ret = command(executable, ["checkout", branch]); + if (ret.code != 0) + throw VcsError.CantCheckoutBranch(this, branch, ret.out); + } + + // return prev. cwd: + Sys.setCwd(oldCwd); + } +} + + +class Mercurial extends Vcs { + + public function new(settings:Settings) + super("hg", "hg", "Mercurial", settings); + + override function searchExecutable():Void { + super.searchExecutable(); + + if (available) + return; + + // if we have already msys git/cmd in our PATH + var match = ~/(.*)hg([\\|\/])cmd$/; + for(path in Sys.getEnv("PATH").split(";")) { + if(match.match(path.toLowerCase())) { + var newPath = match.matched(1) + executable + match.matched(2) + "bin"; + Sys.putEnv("PATH", Sys.getEnv("PATH") + ";" + newPath); + } + } + checkExecutable(); + } + + override public function update(libName:String):Bool { + command(executable, ["pull"]); + var summary = command(executable, ["summary"]).out; + var diff = command(executable, ["diff", "-U", "2", "--git", "--subrepos"]); + var status = command(executable, ["status"]); + + // get new pulled changesets: + // (and search num of sets) + summary = summary.substr(0, summary.length - 1); + summary = summary.substr(summary.lastIndexOf("\n") + 1); + // we don't know any about locale then taking only Digit-exising:s + var changed = ~/(\d)/.match(summary); + if (changed && !settings.quiet) + // print new pulled changesets: + Sys.println(summary); + + + if (diff.code + status.code + diff.out.length + status.out.length != 0) { + if (!settings.quiet) + Sys.println(diff.out); + if (Cli.ask("Reset changes to " + libName + " " + name + " repo so we can update to latest version?")) { + sure(command(executable, ["update", "--clean"])); + } else { + changed = false; + if (!settings.quiet) + Sys.println(name + " repo left untouched"); + } + } else if (changed) { + sure(command(executable, ["update"])); + } + + return changed; + } + + override public function clone(libPath:String, url:String, ?branch:String, ?version:String):Void { + var vcsArgs = ["clone", url, libPath]; + + if (branch != null) { + vcsArgs.push("--branch"); + vcsArgs.push(branch); + } + + if (version != null) { + vcsArgs.push("--rev"); + vcsArgs.push(version); + } + + if (command(executable, vcsArgs).code != 0) + throw VcsError.CantCloneRepo(this, url/*, ret.out*/); + } +} diff --git a/tools/haxelib/server/FileStorage.hx b/tools/haxelib/server/FileStorage.hx new file mode 100644 index 000000000..97922d68e --- /dev/null +++ b/tools/haxelib/server/FileStorage.hx @@ -0,0 +1,284 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib.server; + +import sys.FileSystem; +import sys.io.*; +import haxe.io.Path; +import haxelib.server.Paths; +import neko.Web; +import aws.*; +import aws.s3.*; +import aws.s3.model.*; +import aws.transfer.*; +using Lambda; + +/** + `FileStorage` is an abstraction to a file system. + It maps relative paths to absolute paths, effectively hides the actual location of the storage. +*/ +class FileStorage { + /** + An static instance of `FileStorage` that everyone use. + One should not create their own instance of `FileStorage` except when testing. + + When both the enviroment variables, HAXELIB_S3BUCKET and AWS_DEFAULT_REGION, are set, + `instance` will be a `S3FileStorage`. Otherwise, it will be a `LocalFileStorage`. + */ + static public var instance(get, null):FileStorage; + static function get_instance() return instance != null ? instance : instance = { + var vars = [ + Sys.getEnv("HAXELIB_S3BUCKET"), + Sys.getEnv("AWS_DEFAULT_REGION") + ]; + switch (vars) { + case [bucket, region] if (vars.foreach(function(v) return v != null && v != "")): + Web.logMessage('using S3FileStorage with bucket $bucket in ${region}'); + new S3FileStorage(Paths.CWD, bucket, region); + case _: + Web.logMessage('using LocalFileStorage'); + new LocalFileStorage(Paths.CWD); + } + } + + /** + Request reading `file` in the function `f`. + `file` should be the relative path to the required file, e.g. `files/3.0/library.zip`. + If the file does not exist, an error will be thrown, and `f` will not be called. + If `file` exist, its abolute path will be given to `f` as input. + It only guarantees `file` exists and the abolute path to it is valid within the call of `f`. + */ + public function readFile(file:RelPath, f:AbsPath->T):T + return throw "should be implemented by subclass"; + + /** + Request writing `file` in the function `f`. + `file` should be a relative path to the required file, e.g. `files/3.0/library.zip`. + Any of the parent directories of `file` that doesn't exist will be created. + The mapped abolute path of `file` will be given to `f` as input. + The abolute path to `file` may and may not contain previously written file. + */ + public function writeFile(file:RelPath, f:AbsPath->T):T + return throw "should be implemented by subclass"; + + /** + Copy existing local `srcFile` to the storage as `dstFile`. + Existing `dstFile` will be overwritten. + If `move` is true, `srcFile` will be deleted, unless `dstFile` happens to located + at the same path of `srcFile`. + */ + public function importFile(srcFile:AbsPath, dstFile:RelPath, move:Bool):Void + throw "should be implemented by subclass"; + + /** + Delete `file` in the storage. + It will be a no-op if `file` does not exist. + */ + public function deleteFile(file:RelPath):Void + throw "should be implemented by subclass"; + + function assertAbsolute(path:String):Void { + #if (haxe_ver >= 3.2) // Path.isAbsolute is added in haxe 3.2 + if (!Path.isAbsolute(path)) + throw '$path is not absolute.'; + #end + } + + function assertRelative(path:String):Void { + #if (haxe_ver >= 3.2) // Path.isAbsolute is added in haxe 3.2 + if (Path.isAbsolute(path)) + throw '$path is not relative.'; + #end + } +} + +class LocalFileStorage extends FileStorage { + /** + The local directory of the file storage. + */ + public var path(default, null):AbsPath; + + /** + Create a `FileStorage` located at a local directory specified by an absolute `path`. + */ + public function new(path:AbsPath):Void { + assertAbsolute(path); + this.path = path; + } + + override public function readFile(file:RelPath, f:AbsPath->T):T { + assertRelative(file); + var file:AbsPath = Path.join([path, file]); + if (!FileSystem.exists(file)) + throw '$file does not exist.'; + return f(file); + } + + override public function writeFile(file:RelPath, f:AbsPath->T):T { + assertRelative(file); + var file:AbsPath = Path.join([path, file]); + FileSystem.createDirectory(Path.directory(file)); + return f(file); + } + + override public function importFile(srcFile:AbsPath, dstFile:RelPath, move:Bool):Void { + var localFile:AbsPath = Path.join([path, dstFile]); + if ( + FileSystem.exists(localFile) && + FileSystem.fullPath(localFile) == FileSystem.fullPath(srcFile) + ) { + // srcFile already located at dstFile + return; + } + FileSystem.createDirectory(Path.directory(localFile)); + File.copy(srcFile, localFile); + if (move) + FileSystem.deleteFile(srcFile); + } + + override public function deleteFile(file:RelPath):Void { + var localFile:AbsPath = Path.join([path, file]); + if (FileSystem.exists(localFile)) + FileSystem.deleteFile(localFile); + } +} + +class S3FileStorage extends FileStorage { + /** + The local directory for caching. + */ + public var localPath(default, null):AbsPath; + + /** + The S3 bucket name. + */ + public var bucketName(default, null):String; + + /** + The region where the S3 bucket is located. + e.g. 'us-east-1' + */ + public var bucketRegion(default, null):aws.Region; + + /** + The public endpoint of the S3 bucket. + e.g. 'http://${bucket}.s3-website-${region}.amazonaws.com/' + */ + public var bucketEndpoint(get, never):String; + function get_bucketEndpoint() + return 'http://${bucketName}.s3-website-${bucketRegion}.amazonaws.com/'; + + var s3Client(default, null):S3Client; + var transferClient(default, null):TransferClient; + + static var awsInited = false; + + public function new(localPath:AbsPath, bucketName:String, bucketRegion:String):Void { + assertAbsolute(localPath); + this.localPath = localPath; + this.bucketName = bucketName; + this.bucketRegion = bucketRegion; + + if (!awsInited) { + Aws.initAPI(); + awsInited = true; + } + + this.transferClient = new TransferClient(this.s3Client = new S3Client(bucketRegion)); + } + + override public function readFile(file:RelPath, f:AbsPath->T):T { + assertRelative(file); + var s3Path = Path.join(['s3://${bucketName}', file]); + var localFile:AbsPath = Path.join([localPath, file]); + if (!FileSystem.exists(localFile)) { + var request = transferClient.downloadFile(localFile, bucketName, file); + while (!request.isDone()) { + Sys.sleep(0.01); + } + switch (request.getFailure()) { + case null: + //pass + case failure: + throw 'failed to download ${s3Path} to ${localPath}\n${failure}'; + } + } + return f(localFile); + } + + function uploadToS3(localFile:AbsPath, file:RelPath, contentType = "application/octet-stream") { + var s3Path = Path.join(['s3://${bucketName}', file]); + var request = transferClient.uploadFile(localFile, bucketName, file, contentType); + while (!request.isDone()) { + Sys.sleep(0.01); + } + switch (request.getFailure()) { + case null: + //pass + case failure: + throw 'failed to upload ${localFile} to ${s3Path}\n${failure}'; + } + } + + override public function writeFile(file:RelPath, f:AbsPath->T):T { + assertRelative(file); + var localFile:AbsPath = Path.join([localPath, file]); + if (!FileSystem.exists(localFile)) + throw '$localFile does not exist'; + FileSystem.createDirectory(Path.directory(localFile)); + var r = f(localFile); + uploadToS3(localFile, file); + return r; + } + + override public function importFile(srcFile:AbsPath, dstFile:RelPath, move:Bool):Void { + var localFile:AbsPath = Path.join([localPath, dstFile]); + if ( + FileSystem.exists(localFile) && + FileSystem.fullPath(localFile) == FileSystem.fullPath(srcFile) + ) { + // srcFile already located at dstFile + uploadToS3(localFile, dstFile); + return; + } + FileSystem.createDirectory(Path.directory(localFile)); + File.copy(srcFile, localFile); + uploadToS3(localFile, dstFile); + if (move) + FileSystem.deleteFile(srcFile); + } + + override public function deleteFile(file:RelPath):Void { + var localFile:AbsPath = Path.join([localPath, file]); + if (FileSystem.exists(localFile)) + FileSystem.deleteFile(localFile); + + var del = new DeleteObjectRequest(); + del.setBucket(bucketName); + del.setKey(file); + try { + s3Client.deleteObject(del); + } catch (e:Dynamic) { + // maybe the object does not exist + } + } +} \ No newline at end of file diff --git a/tools/haxelib/server/Paths.hx b/tools/haxelib/server/Paths.hx new file mode 100644 index 000000000..ed8205a12 --- /dev/null +++ b/tools/haxelib/server/Paths.hx @@ -0,0 +1,54 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib.server; + +import neko.Web; +import haxe.io.*; + +/** + Absolute path. +*/ +typedef AbsPath = String; + +/** + Relative path. +*/ +typedef RelPath = String; + +class Paths { + static public var CWD(default, null):AbsPath = + #if haxelib_api + Path.normalize(Path.join([Web.getCwd(), "..", ".."])); + #elseif haxelib_legacy + Path.normalize(Path.join([Web.getCwd(), ".."])); + #else + Web.getCwd(); + #end + static public var DB_CONFIG_NAME(default, null):RelPath = "dbconfig.json"; + static public var DB_CONFIG(default, null):AbsPath = Path.join([CWD, DB_CONFIG_NAME]); + static public var DB_FILE_NAME(default, null):RelPath = "haxelib.db"; + static public var DB_FILE(default, null):AbsPath = Path.join([CWD, DB_FILE_NAME]); + + static public var TMP_DIR_NAME(default, null):RelPath = "tmp"; + static public var TMP_DIR(default, null):AbsPath = Path.join([CWD, TMP_DIR_NAME]); + static public var REP_DIR_NAME(default, null):RelPath = Data.REPOSITORY; +} \ No newline at end of file diff --git a/tools/haxelib/server/Repo.hx b/tools/haxelib/server/Repo.hx new file mode 100644 index 000000000..2bbf2faca --- /dev/null +++ b/tools/haxelib/server/Repo.hx @@ -0,0 +1,368 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib.server; + +import haxe.io.*; +import neko.Web; +import sys.io.*; +import sys.*; + +import haxelib.Data; +import haxelib.SemVer; +import haxelib.server.Paths.*; +import haxelib.server.SiteDb; +import haxelib.server.FileStorage; + +class Repo implements SiteApi { + + static function run() { + FileSystem.createDirectory(TMP_DIR); + + var ctx = new haxe.remoting.Context(); + ctx.addObject("api", new Repo()); + + if( haxe.remoting.HttpConnection.handleRequest(ctx) ) + return; + else + throw "Invalid remoting call"; + } + + public function new() {} + + public function search( word : String ) : List<{ id : Int, name : String }> { + return Project.containing(word); + } + + public function infos( project : String ) : ProjectInfos { + var p = Project.manager.select($name == project); + if( p == null ) + throw "No such Project : "+project; + var vl = Version.manager.search($project == p.id); + + var sumDownloads = function(version:Version, total:Int) return total += version.downloads; + var totalDownloads = Lambda.fold(vl, sumDownloads, 0); + + return { + name : p.name, + curversion : if( p.versionObj == null ) null else p.versionObj.toSemver(), + desc : p.description, + versions: + [for ( v in vl ) { + name : v.toSemver(), + comments : v.comments, + downloads : v.downloads, + date : v.date + }], + owner : p.ownerObj.name, + website : p.website, + license : p.license, + downloads : totalDownloads, + tags : Tag.manager.search($project == p.id).map(function(t) return t.tag), + }; + } + + public function getLatestVersion( project : String ) : SemVer { + var p = Project.manager.select($name == project); + if( p == null ) + throw "No such Project : "+project; + + var vl = Version.manager.unsafeObjects('SELECT * FROM Version WHERE project = ${p.id} ORDER BY major DESC, minor DESC, patch DESC, ifnull(preview, 100) DESC, previewNum DESC LIMIT 1', false); + return vl.first().toSemver(); + } + + public function user( name : String ) : UserInfos { + var u = User.manager.search($name == name).first(); + if( u == null ) + throw "No such user : "+name; + var pl = Project.manager.search($owner == u.id); + var projects = new Array(); + for( p in pl ) + projects.push(p.name); + return { + name : u.name, + fullname : u.fullname, + email : u.email, + projects : projects, + }; + } + + public function register( name : String, pass : String, mail : String, fullname : String ) : Void { + if( name.length < 3 ) + throw "User name must be at least 3 characters"; + if( !Data.alphanum.match(name) ) + throw "Invalid user name, please use alphanumeric characters"; + if( User.manager.count($name == name) > 0 ) + throw 'User name "$name" is already taken'; + var u = new User(); + u.name = name; + u.pass = pass; + u.email = mail; + u.fullname = fullname; + u.insert(); + } + + public function isNewUser( name : String ) : Bool { + return User.manager.select($name == name) == null; + } + + public function checkDeveloper( prj : String, user : String ) : Void { + var p = Project.manager.search({ name : prj }).first(); + if( p == null ) + return; + for( d in Developer.manager.search({ project : p.id }) ) + if( d.userObj.name == user ) + return; + throw "User '"+user+"' is not a developer of project '"+prj+"'"; + } + + public function checkPassword( user : String, pass : String ) : Bool { + var u = User.manager.search({ name : user }).first(); + return u != null && u.pass == pass; + } + + public function getSubmitId() : String { + return Std.string(Std.random(100000000)); + } + + public function processSubmit( id : String, user : String, pass : String ) : String { + var tmpFile = Path.join([TMP_DIR_NAME, Std.parseInt(id)+".tmp"]); + return FileStorage.instance.readFile( + tmpFile, + function(path):String { + var file = try sys.io.File.read(path,true) catch( e : Dynamic ) throw "Invalid file id #"+id; + var zip = try haxe.zip.Reader.readZip(file) catch( e : Dynamic ) { file.close(); neko.Lib.rethrow(e); }; + file.close(); + + var infos = Data.readInfos(zip,true); + var u = User.manager.search({ name : user }).first(); + if( u == null || u.pass != pass ) + throw "Invalid username or password"; + + var devs = infos.contributors.map(function(user) { + var u = User.manager.search({ name : user }).first(); + if( u == null ) + throw "Unknown user '"+user+"'"; + return u; + }); + + var tags = Lambda.array(infos.tags); + tags.sort(Reflect.compare); + + var p = Project.manager.search({ name : infos.name }).first(); + + // create project if needed + if( p == null ) { + p = new Project(); + p.name = infos.name; + p.description = infos.description; + p.website = infos.url; + p.license = infos.license; + p.ownerObj = u; + p.insert(); + for( u in devs ) { + var d = new Developer(); + d.userObj = u; + d.projectObj = p; + d.insert(); + } + for( tag in tags ) { + var t = new Tag(); + t.tag = tag; + t.projectObj = p; + t.insert(); + } + } + + // check submit rights + var pdevs = Developer.manager.search({ project : p.id }); + var isdev = false; + for( d in pdevs ) + if( d.userObj.id == u.id ) { + isdev = true; + break; + } + if( !isdev ) + throw "You are not a developer of this project"; + + var otags = Tag.manager.search({ project : p.id }); + var curtags = otags.map(function(t) return t.tag).join(":"); + + var devsChanged = (pdevs.length != devs.length); + if (!devsChanged) { // same length, check whether elements are the same + for (d in pdevs) { + var exists = Lambda.exists(devs, function(u) return u.id == d.userObj.id); + if (!exists) { + devsChanged = true; + break; + } + } + } + + // update public infos + if( devsChanged || infos.description != p.description || p.website != infos.url || p.license != infos.license || tags.join(":") != curtags ) { + if( u.id != p.ownerObj.id ) + throw "Only project owner can modify project infos"; + p.description = infos.description; + p.website = infos.url; + p.license = infos.license; + p.update(); + if( devsChanged ) { + for( d in pdevs ) + d.delete(); + for( u in devs ) { + var d = new Developer(); + d.userObj = u; + d.projectObj = p; + d.insert(); + } + } + if( tags.join(":") != curtags ) { + for( t in otags ) + t.delete(); + for( tag in tags ) { + var t = new Tag(); + t.tag = tag; + t.projectObj = p; + t.insert(); + } + } + } + + // look for current version + var current = null; + for( v in Version.manager.search({ project : p.id }) ) + if( v.name == infos.version ) { + current = v; + break; + } + + // update documentation + var doc = null; + var docXML = Data.readDoc(zip); + if( docXML != null ) { + try { + var p = new haxe.rtti.XmlParser(); + p.process(Xml.parse(docXML).firstElement(),null); + p.sort(); + var roots = new Array(); + for( x in p.root ) + switch( x ) { + case TPackage(name,_,_): + switch( name ) { + case "flash","sys","cs","java","haxe","js","neko","cpp","php","python": // don't include haXe core types + default: roots.push(x); + } + default: + // don't include haXe root types + } + var s = new haxe.Serializer(); + s.useEnumIndex = true; + s.useCache = true; + s.serialize(roots); + doc = s.toString(); + } catch ( e:Dynamic ) { + // If documentation can't be generated, ignore it. + } + } + + // update file + var fileName = Data.fileName(p.name, infos.version); + var storage = FileStorage.instance; + storage.importFile(path, Path.join([Paths.REP_DIR_NAME, fileName]), true); + storage.deleteFile(tmpFile); + + var semVer = SemVer.ofString(infos.version); + + // update existing version + if( current != null ) { + current.documentation = doc; + current.comments = infos.releasenote; + current.update(); + return "Version "+current.name+" (id#"+current.id+") updated"; + } + + // add new version + var v = new Version(); + v.projectObj = p; + v.major = semVer.major; + v.minor = semVer.minor; + v.patch = semVer.patch; + v.preview = semVer.preview; + v.previewNum = semVer.previewNum; + + v.comments = infos.releasenote; + v.downloads = 0; + v.date = Date.now().toString(); + v.documentation = doc; + v.insert(); + + // p.versionObj is the one shown on the website + if (p.versionObj == null || p.versionObj.toSemver() < v.toSemver()) { + p.versionObj = v; + } + + p.update(); + return "Version " + v.toSemver() + " (id#" + v.id + ") added"; + } + ); + } + + public function postInstall( project : String, version : String ) { + var p = Project.manager.select($name == project); + if( p == null ) + throw "No such Project : " + project; + + var version = SemVer.ofString(version); + // don't use macro select because of + // https://github.com/HaxeFoundation/haxe/issues/4931 + // and https://github.com/HaxeFoundation/haxe/issues/4932 + var v = Version.manager.dynamicSearch({ + project: p.id, + major: version.major, + minor: version.minor, + patch: version.patch, + preview: if (version.preview == null) null else version.preview.getIndex(), + previewNum: version.previewNum + }).first(); + + if( v == null ) + throw "No such Version : " + version; + v.downloads++; + v.update(); + p.downloads++; + p.update(); + } + + static function main() { + var error = null; + SiteDb.init(); + try { + run(); + } catch( e : Dynamic ) { + error = { e : e }; + } + SiteDb.cleanup(); + if( error != null ) + neko.Lib.rethrow(error.e); + } + +} diff --git a/tools/haxelib/server/SiteDb.hx b/tools/haxelib/server/SiteDb.hx new file mode 100644 index 000000000..482a0e71a --- /dev/null +++ b/tools/haxelib/server/SiteDb.hx @@ -0,0 +1,159 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +package haxelib.server; + +import sys.db.*; +import sys.db.Types; +import haxelib.server.Paths.*; + +class User extends Object { + + public var id : SId; + public var name : String; + public var fullname : String; + public var email : String; + public var pass : String; + +} + +class Project extends Object { + + public var id : SId; + public var name : String; + public var description : String; + public var website : String; + public var license : String; + public var downloads : Int = 0; + @:relation(owner) public var ownerObj : User; + @:relation(version) public var versionObj : SNull; + + static public function containing( word:String ) : List<{ id: Int, name: String }> { + var ret = new List(); + word = '%$word%'; + for (project in manager.search($name.like(word) || $description.like(word))) + ret.push( { id: project.id, name: project.name } ); + return ret; + } + + static public function allByName() { + //TODO: Propose SPOD patch to support manager.search(true, { orderBy: name.toLowerCase() } ); + return manager.unsafeObjects('SELECT * FROM Project ORDER BY LOWER(name)', false); + } + +} + +class Tag extends Object { + + public var id : SId; + public var tag : String; + @:relation(project) public var projectObj : Project; + + static public function topTags( n : Int ) : List<{ tag:String, count: Int }> { + return cast Manager.cnx.request("SELECT tag, COUNT(*) as count FROM Tag GROUP BY tag ORDER BY count DESC LIMIT " + n).results(); + } + +} + +class Version extends Object { + + public var id : SId; + @:relation(project) public var projectObj : Project; + public var major : Int; + public var minor : Int; + public var patch : Int; + public var preview : SNull>; + public var previewNum : SNull; + @:skip public var name(get, never):String; + function get_name():String return toSemver(); + + public function toSemver():SemVer { + return { + major: this.major, + minor: this.minor, + patch: this.patch, + preview: this.preview, + previewNum: this.previewNum, + } + } + public var date : String; // sqlite does not have a proper 'date' type + public var comments : String; + public var downloads : Int; + public var documentation : SNull; + + static public function latest( n : Int ) { + return manager.search(1 == 1, { orderBy: -date, limit: n } ); + } + + static public function byProject( p : Project ) { + return manager.search($project == p.id, { orderBy: -date } ); + } + +} + +@:id(user,project) +class Developer extends Object { + + @:relation(user) public var userObj : User; + @:relation(project) public var projectObj : Project; + +} + +class SiteDb { + static var db : Connection; + //TODO: this whole configuration business is rather messy to say the least + + static public function init() { + db = + if (Sys.getEnv("HAXELIB_DB_HOST") != null) + Mysql.connect({ + "host": Sys.getEnv("HAXELIB_DB_HOST"), + "port": Std.parseInt(Sys.getEnv("HAXELIB_DB_PORT")), + "database": Sys.getEnv("HAXELIB_DB_NAME"), + "user": Sys.getEnv("HAXELIB_DB_USER"), + "pass": Sys.getEnv("HAXELIB_DB_PASS"), + "socket": null + }); + else if (sys.FileSystem.exists(DB_CONFIG)) + Mysql.connect(haxe.Json.parse(sys.io.File.getContent(DB_CONFIG))); + else + Sqlite.open(DB_FILE); + + Manager.cnx = db; + Manager.initialize(); + + var managers:Array> = [ + User.manager, + Project.manager, + Tag.manager, + Version.manager, + Developer.manager + ]; + for (m in managers) + if (!TableCreate.exists(m)) + TableCreate.create(m); + } + + static public function cleanup() { + db.close(); + Manager.cleanup(); + } +} diff --git a/tools/tools.hxml b/tools/tools.hxml index fff56ca9f..e8a1eb2df 100644 --- a/tools/tools.hxml +++ b/tools/tools.hxml @@ -6,4 +6,9 @@ -cp .. -lib format #-lib svg --D optional-cffi \ No newline at end of file +-D optional-cffi + +--next + +-neko haxelib.n +-main haxelib.client.Main \ No newline at end of file diff --git a/tools/utils/PlatformSetup.hx b/tools/utils/PlatformSetup.hx index 1da491d90..34e9a0d9e 100644 --- a/tools/utils/PlatformSetup.hx +++ b/tools/utils/PlatformSetup.hx @@ -367,7 +367,7 @@ class PlatformSetup { if (version != null && version.indexOf ("*") > -1) { var regexp = new EReg ("^.+[0-9]+-[0-9]+-[0-9]+ +[0-9]+:[0-9]+:[0-9]+ +([a-z0-9.-]+) +", "gi"); - var output = ProcessHelper.runProcess ("", "haxelib", [ "info", haxelib.name ]); + var output = HaxelibHelper.runProcess ("", [ "info", haxelib.name ]); var lines = output.split ("\n"); var versions = new Array (); @@ -410,7 +410,7 @@ class PlatformSetup { } - ProcessHelper.runCommand ("", "haxelib", args); + HaxelibHelper.runCommand ("", args); } @@ -759,7 +759,7 @@ class PlatformSetup { } - ProcessHelper.runCommand ("", "haxelib", [ "install", "air3" ], true, true); + HaxelibHelper.runCommand ("", [ "install", "air3" ], true, true); } @@ -1482,7 +1482,7 @@ class PlatformSetup { var defines = new Map (); defines.set ("setup", 1); - var basePath = ProcessHelper.runProcess ("", "haxelib", [ "config" ]); + var basePath = HaxelibHelper.runProcess ("", [ "config" ]); if (basePath != null) { basePath = StringTools.trim (basePath.split ("\n")[0]); @@ -1647,7 +1647,7 @@ class PlatformSetup { writeConfig (defines.get ("LIME_CONFIG"), defines); - ProcessHelper.runCommand ("", "haxelib", [ "install", "cordova" ], true, true); + HaxelibHelper.runCommand ("", [ "install", "cordova" ], true, true); } @@ -2117,7 +2117,7 @@ class PlatformSetup { public static function updateHaxelib (haxelib:Haxelib):Void { - var basePath = ProcessHelper.runProcess ("", "haxelib", [ "config" ]); + var basePath = HaxelibHelper.runProcess ("", [ "config" ]); if (basePath != null) { @@ -2129,7 +2129,7 @@ class PlatformSetup { if (StringTools.startsWith (PathHelper.standardize (lib), PathHelper.standardize (basePath))) { - ProcessHelper.runCommand ("", "haxelib", [ "update", haxelib.name ]); + HaxelibHelper.runCommand ("", [ "update", haxelib.name ]); } else {