├── .haxerc ├── .gitignore ├── src └── switchx │ ├── Fs.hx │ ├── Unzip.hx │ ├── Yauzl.hx │ ├── Tar.hx │ ├── Version.hx │ ├── Command.hx │ ├── Cli.hx │ ├── Switchx.hx │ └── Download.hx ├── .gitmodules ├── .vscode ├── settings.json └── tasks.json ├── switchx.hxml ├── haxe_libraries ├── tink_core.hxml ├── haxeshim.hxml └── hxnodejs.hxml ├── .travis.yml ├── package.json ├── LICENSE └── README.md /.haxerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.4.2" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/switchx.js 2 | /node_modules 3 | -------------------------------------------------------------------------------- /src/switchx/Fs.hx: -------------------------------------------------------------------------------- 1 | package switchx; 2 | 3 | typedef Fs = haxeshim.Fs; -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/haxeshim"] 2 | path = lib/haxeshim 3 | url = https://github.com/lix-pm/haxeshim 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "haxe.displayConfigurations": [ 3 | ["switchx.hxml"] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /switchx.hxml: -------------------------------------------------------------------------------- 1 | -lib haxeshim 2 | -cp src 3 | -js bin/switchx.js 4 | -main switchx.Cli 5 | --macro haxeshim.Build.postprocess() -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "haxe", 4 | "args": ["switchx.hxml", "--next", "-cmd", "node bin/switchx.js"], 5 | "problemMatcher": "$haxe" 6 | } 7 | -------------------------------------------------------------------------------- /haxe_libraries/tink_core.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:tink_core#1.15.0" into tink_core/1.15.0/haxelib 2 | -D tink_core=1.15.0 3 | -cp ${HAXESHIM_LIBCACHE}/tink_core/1.15.0/haxelib/src 4 | -------------------------------------------------------------------------------- /src/switchx/Unzip.hx: -------------------------------------------------------------------------------- 1 | package switchx; 2 | 3 | import js.node.stream.Writable.IWritable; 4 | 5 | @:jsRequire('unzipper') 6 | extern class Unzip { 7 | static function Extract(options: { path:String, ?strip:Int } ):IWritable; 8 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: node_js 5 | node_js: 6 6 | 7 | os: 8 | - linux 9 | 10 | install: 11 | - npm install haxeshim -g 12 | - npm install lix.pm -g 13 | - lix download 14 | - haxe switchx.hxml 15 | - npm install . -g 16 | 17 | script: 18 | 19 | - switchx list -------------------------------------------------------------------------------- /haxe_libraries/haxeshim.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "https://github.com/lix-pm/haxeshim/archive/cd11cb200396ec2dd3a620b0a146e193b9b7e63f.tar.gz" into haxeshim/0.0.0/github/cd11cb200396ec2dd3a620b0a146e193b9b7e63f 2 | -D haxeshim=0.0.0 3 | -cp ${HAXESHIM_LIBCACHE}/haxeshim/0.0.0/github/cd11cb200396ec2dd3a620b0a146e193b9b7e63f/src 4 | 5 | -lib tink_core 6 | -lib hxnodejs -------------------------------------------------------------------------------- /haxe_libraries/hxnodejs.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download https://github.com/HaxeFoundation/hxnodejs/archive/cf80c6a077e705d39f752418e95555b346f4d9b2.tar.gz into hxnodejs/6.9.0/github/cf80c6a077e705d39f752418e95555b346f4d9b2 2 | -D hxnodejs=6.9.0 3 | -cp ${HAXESHIM_LIBCACHE}/hxnodejs/6.9.0/github/cf80c6a077e705d39f752418e95555b346f4d9b2/src 4 | -D nodejs 5 | --macro allowPackage('sys') 6 | --macro _hxnodejs.VersionWarning.include() 7 | -------------------------------------------------------------------------------- /src/switchx/Yauzl.hx: -------------------------------------------------------------------------------- 1 | package switchx; 2 | import js.node.Buffer; 3 | import js.node.stream.Readable.IReadable; 4 | 5 | @:jsRequire("yauzl") 6 | extern class Yauzl { 7 | 8 | static function fromBuffer(buf:Buffer, cb:js.Error->YauzlArchive->Void):Void; 9 | 10 | } 11 | 12 | extern interface YauzlArchive extends js.node.events.EventEmitter.IEventEmitter { 13 | var entriesRead(default, null):Int; 14 | var entryCount(default, null):Int; 15 | function openReadStream(entry:Dynamic, cb:js.Error->IReadable-> Void):Void; 16 | function close():Void; 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "switchx", 3 | "version": "0.14.6", 4 | "bin": { 5 | "switchx": "bin/switchx.js" 6 | }, 7 | "scripts": { 8 | "prepublishOnly": "haxe switchx.hxml" 9 | }, 10 | "description": "Switch Haxe versions like a sir.", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/lix-pm/switchx.git" 14 | }, 15 | "dependencies": { 16 | "tar": "=4.0.2", 17 | "yauzl": "=2.6.0" 18 | }, 19 | "keywords": [ 20 | "haxe" 21 | ], 22 | "files": [ 23 | "bin" 24 | ], 25 | "author": "back2dos", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/lix-pm/switchx/issues" 29 | }, 30 | "homepage": "https://github.com/lix-pm/switchx#readme" 31 | } 32 | -------------------------------------------------------------------------------- /src/switchx/Tar.hx: -------------------------------------------------------------------------------- 1 | package switchx; 2 | 3 | import js.node.stream.Writable; 4 | import js.node.stream.Readable; 5 | 6 | using tink.CoreApi; 7 | 8 | class Tar { 9 | static public function parse(source:IReadable, onentry:TarEntry->Void):Promise 10 | return Future.async(function (cb) { 11 | var parse = new TarParse({ onentry: onentry }); 12 | source.pipe(parse, { end: true }); 13 | parse.on('end', function () cb(Success(Noise))); 14 | parse.on('error', function (e) cb(Failure(new Error((e.message:String))))); 15 | }); 16 | } 17 | 18 | @:jsRequire('tar', 'Parse') 19 | extern class TarParse extends Writable { 20 | public function new(options:{ function onentry(entry:TarEntry):Void; }):Void; 21 | } 22 | 23 | extern interface TarEntry extends IReadable { 24 | var size(default, null):Int; 25 | var path(default, null):String; 26 | var mode(default, null):Int; 27 | var linkpath(default, null):String; 28 | var type(default, null):TarEntryType; 29 | } 30 | 31 | @:enum abstract TarEntryType(String) to String { 32 | var SymbolicLink = 'SymbolicLink'; 33 | var File = 'File'; 34 | var Directory = 'Directory'; 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lix-pm/Lobby) 2 | 3 | # switchx - Switch Haxe versions like a sir. 4 | 5 | This little tool is based on [haxeshim](https://github.com/lix-pm/haxeshim) to switch between coexisting Haxe versions. As for usage, the command line doc pretty much says it all: 6 | 7 | ``` 8 | switchx - haxe version switcher 9 | 10 | Supported commands: 11 | 12 | install [] : installs the version if specified, otherwise 13 | installs the currently configured version 14 | download : downloads the specified version 15 | use : switches to the specified version 16 | scope [create|delete|set] : creates, deletes or configures 17 | [scoped|mixed|haxelib] the current scope or inspects it 18 | if no argument is supplied 19 | list : lists currently downloaded versions 20 | 21 | Supported switches 22 | 23 | --silent : disables logging 24 | --global : performs operation on global scope 25 | --force : forces re-download 26 | 27 | Version aliases 28 | 29 | edge, nightly : latest nightly build from builds.haxe.org 30 | latest : latest official release from haxe.org 31 | stable : latest stable release from haxe.org 32 | ``` 33 | 34 | Note that in `switch` version aliases refer to the latest *installed* version of that kind while otherwise they refer to the latest version *found online*. Please refer to the [haxeshim doc for library resolution strategies](https://github.com/lix-pm/haxeshim#library-resolution) 35 | 36 | ## Installation 37 | 38 | Not as smooth as it could be, but `npm install haxeshim -g && npm install switchx -g && switchx` basically kind of does it. 39 | 40 | ## OS support 41 | 42 | For the most parts, please refer to the [haxeshim documentation](https://github.com/lix-pm/haxeshim#os-support). Note though that currently on linux the 64 bit version is always installed. This is a matter of initializing `Switchx.PLATFORM` right. 43 | 44 | ### Building 45 | 46 | Ah, here comes the fun part. The simplest way right now is to: 47 | 48 | 1. install `switchx` first (through npm) 49 | 2. clone the source recursively and then run `switchx install` in the checked out directory 50 | 3. install node dependencies with `npm install` 51 | 4. build with `haxe switchx.hxml`. 52 | -------------------------------------------------------------------------------- /src/switchx/Version.hx: -------------------------------------------------------------------------------- 1 | package switchx; 2 | 3 | using StringTools; 4 | using haxe.io.Path; 5 | 6 | enum UserVersionData { 7 | UEdge; 8 | ULatest; 9 | UStable; 10 | UNightly(hash:String); 11 | UOfficial(version:Official); 12 | UCustom(path:String); 13 | } 14 | 15 | abstract Official(String) from String to String { 16 | public var isPrerelease(get, never):Bool; 17 | function get_isPrerelease() 18 | return this.indexOf('-') != -1; 19 | 20 | static var SPLITTER = ~/[^0-9a-z]/g; 21 | 22 | static function isNumber(s:String) 23 | return ~/^[0-9]*$/.match(s); 24 | 25 | static function fragment(a:String, b:String) 26 | return 27 | if (isNumber(a) && isNumber(b)) 28 | Std.parseInt(a) - Std.parseInt(b); 29 | else 30 | Reflect.compare(a, b); 31 | 32 | static public function compare(a:Official, b:Official):Int { 33 | 34 | var a = (a:String).split('-'), 35 | b = (b:String).split('-'), 36 | i = 0; 37 | 38 | while (i < a.length && i < b.length) { 39 | var a = a[i].split('.'), 40 | b = b[i++].split('.'), 41 | i = 0; 42 | while (i < a.length && i < b.length) 43 | switch fragment(a[i], b[i]) { 44 | case 0: i++; 45 | case v: return -v; 46 | } 47 | 48 | switch a.length - b.length { 49 | case 0: 50 | case v: return -v; 51 | } 52 | } 53 | 54 | return 55 | (a.length - b.length) * (if (i == 1) 1 else -1); 56 | } 57 | } 58 | 59 | enum ResolvedUserVersionData { 60 | RNightly(nightly:Nightly); 61 | ROfficial(version:Official); 62 | RCustom(path:String); 63 | } 64 | 65 | typedef Nightly = { 66 | var hash(default, null):String; 67 | var published(default, null):Date; 68 | } 69 | 70 | abstract ResolvedVersion(ResolvedUserVersionData) from ResolvedUserVersionData to ResolvedUserVersionData { 71 | 72 | public var id(get, never):String; 73 | 74 | function get_id() 75 | return switch this { 76 | case RNightly({ hash: v }): v; 77 | case ROfficial(v) | RCustom(v): v; 78 | } 79 | 80 | public function toString():String 81 | return (this : UserVersion).toString(); 82 | } 83 | 84 | abstract UserVersion(UserVersionData) from UserVersionData to UserVersionData { 85 | 86 | static var hex = [for (c in '0123456789abcdefABCDEF'.split('')) c.charCodeAt(0) => true]; 87 | 88 | @:from static function ofResolved(v:ResolvedVersion):UserVersion 89 | return switch v { 90 | case ROfficial(version): UOfficial(version); 91 | case RNightly({ hash: version }): UNightly(version); 92 | case RCustom(v): UCustom(v); 93 | } 94 | 95 | static public function isHash(version:String) { 96 | 97 | for (i in 0...version.length) 98 | if (!hex[version.fastCodeAt(i)]) 99 | return false; 100 | 101 | return true; 102 | } 103 | 104 | public function toString() 105 | return switch this { 106 | case UEdge: 'latest nightly build'; 107 | case ULatest: 'latest official'; 108 | case UStable: 'latest stable release'; 109 | case UNightly(v): 'nightly build $v'; 110 | case UOfficial(v): 'official release $v'; 111 | case UCustom(v): 'custom version at `$v`'; 112 | } 113 | 114 | static public function isPath(v:String) 115 | return v.isAbsolute() || v.charAt(0) == '.'; 116 | 117 | @:from static public function ofString(s:Null):UserVersion 118 | return 119 | if (s == null) null; 120 | else switch s { 121 | case 'auto': null; 122 | case 'edge' | 'nightly': UEdge; 123 | case 'latest': ULatest; 124 | case 'stable': UStable; 125 | case isHash(_) => true: UNightly(s); 126 | case isPath(_) => true: UCustom(s); 127 | default: UOfficial(s);//TODO: check if this is valid? 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /src/switchx/Command.hx: -------------------------------------------------------------------------------- 1 | package switchx; 2 | 3 | import js.Node.*; 4 | import Sys.*; 5 | 6 | using StringTools; 7 | using tink.CoreApi; 8 | 9 | abstract CommandExpander(Array->Option>) from Array->Option> { 10 | 11 | public inline function expand(args:Array) 12 | return this(args); 13 | 14 | @:from static public function ofString(s:String):CommandExpander 15 | return switch s.indexOf(' ') { 16 | case -1: 17 | throw 'invalid expander syntax in "$s"'; 18 | case v: 19 | make(s.substr(0, v), s.substr(v + 1)); 20 | } 21 | 22 | static public function make(prefix:String, rule:String):CommandExpander { 23 | 24 | var replacer = ~/\$\{([0-9]+)\}/g; 25 | var parts = []; 26 | var highest = -1; 27 | replacer.map(rule, function (e) { 28 | 29 | parts.pop(); 30 | 31 | var left = e.matchedLeft(), 32 | right = e.matchedRight(); 33 | 34 | parts.push(function (_) return left); 35 | 36 | var pos = Std.parseInt(e.matched(1)); 37 | 38 | if (pos > highest) 39 | highest = pos; 40 | 41 | parts.push(function (args:Array) return args[pos]); 42 | parts.push(function (_) return right); 43 | 44 | return ''; 45 | }); 46 | 47 | function apply(args:Array) 48 | return 49 | [for (res in [for (p in parts) p(args)].join('').split(' ')) 50 | switch res.trim() { 51 | case '': continue; 52 | case v: v; 53 | } 54 | ]; 55 | 56 | return function (args:Array) { 57 | for (i in 0...args.length) 58 | if (args[i] == prefix) 59 | return Some( 60 | args.slice(0, i) 61 | .concat(apply(args.slice(i + 1, i + highest + 2))) 62 | .concat(args.slice(i + highest + 2)) 63 | ); 64 | return None; 65 | } 66 | } 67 | } 68 | 69 | abstract CommandName(Array) from Array { 70 | 71 | @:from static function ofString(s:String):CommandName 72 | return [s]; 73 | 74 | @:to public function toString() 75 | return switch this { 76 | case [v]: v; 77 | case a: a.join(' / '); 78 | } 79 | 80 | @:op(a == b) static public function eq(a:CommandName, b:String) 81 | return (cast a : Array).indexOf(b) != -1; 82 | } 83 | 84 | class Command { 85 | 86 | public var name(default, null):CommandName; 87 | public var args(default, null):String; 88 | public var doc(default, null):String; 89 | public var exec(default, null):Array->Promise; 90 | 91 | public function new(name, args, doc, exec) { 92 | this.name = name; 93 | this.args = args; 94 | this.doc = doc; 95 | this.exec = exec; 96 | } 97 | 98 | public function as(alias:CommandName, ?doc:String) 99 | return new Command(alias, args, if (doc == null) this.doc else doc, exec); 100 | 101 | static public function reportError(e:Error):Dynamic { 102 | stderr().writeString(e.message + '\n\n'); 103 | Sys.exit(e.code); 104 | return null; 105 | } 106 | 107 | static public function reportOutcome(o:Outcome) 108 | switch o { 109 | case Failure(e): reportError(e); 110 | default: 111 | } 112 | 113 | static public function expand(args:Array, expanders:Array) { 114 | var changed = true; 115 | while (changed) { 116 | changed = false; 117 | 118 | for (e in expanders) 119 | switch e.expand(args) { 120 | case Some(nu): 121 | args = nu; 122 | changed = true; 123 | default: 124 | } 125 | } 126 | return args; 127 | } 128 | 129 | static public function dispatch(args:Array, title:String, commands:Array, extras:Array>>>):Promise 130 | return 131 | switch args.shift() { 132 | case null | '--help': 133 | println(title); 134 | println(''); 135 | var prefix = 0; 136 | 137 | for (c in commands) { 138 | var longest = { 139 | var v = 0; 140 | for (line in c.args.split('\n')) 141 | if (line.length > v) v = line.length; 142 | v; 143 | } 144 | var cur = c.name.toString().length + longest; 145 | if (cur > prefix) 146 | prefix = cur; 147 | } 148 | 149 | prefix += 7; 150 | 151 | function pad(s:String) 152 | return s.lpad(' ', prefix); 153 | 154 | println(' Supported commands:'); 155 | println(''); 156 | 157 | for (c in commands) { 158 | var leftCol = c.args.split('\n'); 159 | 160 | leftCol[0] = ' ' + c.name + (switch leftCol[0] { case '' | null: ''; case v: ' $v'; }) + ' : '; 161 | for (i in 1...leftCol.length) 162 | leftCol[i] = leftCol[i] + ' '; 163 | var rightCol = c.doc.split('\n'); 164 | 165 | while (leftCol.length < rightCol.length) 166 | leftCol.push(''); 167 | 168 | while (leftCol.length > rightCol.length) 169 | rightCol.push(''); 170 | 171 | for (i in 0...leftCol.length) 172 | println(pad(leftCol[i]) + rightCol[i]); 173 | } 174 | 175 | for (e in extras) { 176 | println(''); 177 | println(' ${e.name}'); 178 | println(''); 179 | for (e in e.value) 180 | println(pad('${e.name} : ') + e.value); 181 | } 182 | println(''); 183 | Noise; 184 | 185 | case command: 186 | 187 | for (canditate in commands) 188 | if (canditate.name == command) 189 | return canditate.exec(args); 190 | 191 | return new Error(NotFound, 'unknown command $command'); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/switchx/Cli.hx: -------------------------------------------------------------------------------- 1 | package switchx; 2 | 3 | import haxeshim.*; 4 | import haxeshim.LibResolution; 5 | import js.Node.*; 6 | import Sys.*; 7 | import switchx.Version; 8 | 9 | using DateTools; 10 | using tink.CoreApi; 11 | using StringTools; 12 | using sys.FileSystem; 13 | 14 | class Cli { 15 | 16 | var api:Switchx; 17 | var force:Bool; 18 | 19 | public function new(api, force) { 20 | this.api = api; 21 | this.force = force; 22 | } 23 | 24 | static public function ensureGlobal(command:String = "switchx") 25 | return 26 | Future.async(function (cb) { 27 | 28 | function done() 29 | cb(Scope.seek()); 30 | 31 | if (Scope.exists(Scope.DEFAULT_ROOT)) done(); 32 | else { 33 | 34 | println('It seems you\'re running $command for the first time.\nPlease wait for basic setup to finish ...'); 35 | 36 | Fs.ensureDir(Scope.DEFAULT_ROOT + '/'); 37 | 38 | Scope.create(Scope.DEFAULT_ROOT, { 39 | version: 'stable', 40 | resolveLibs: Mixed, 41 | }); 42 | 43 | dispatch(['install', '--global'], function () { 44 | println('... done setting up global Haxe version'); 45 | done(); 46 | }); 47 | } 48 | }); 49 | 50 | static function ensureNeko(global:Scope) { 51 | 52 | var neko = Neko.PATH; 53 | 54 | return 55 | if (neko.exists()) 56 | Future.sync(neko); 57 | else { 58 | 59 | println('Neko seems to be missing. Attempting download ...'); 60 | 61 | (switch systemName() { 62 | case 'Windows': Download.zip.bind('https://github.com/HaxeFoundation/neko/releases/download/v2-2-0/neko-2.2.0-win.zip'); 63 | case 'Mac': Download.tar.bind('https://github.com/HaxeFoundation/neko/releases/download/v2-2-0/neko-2.2.0-osx64.tar.gz'); 64 | default: Download.tar.bind('https://github.com/HaxeFoundation/neko/releases/download/v2-2-0/neko-2.2.0-linux64.tar.gz'); 65 | })(1, neko).recover(Command.reportError).map(function (x) { 66 | println('done'); 67 | return x; 68 | }); 69 | } 70 | } 71 | 72 | static function main() { 73 | ensureGlobal().flatMap(ensureNeko).handle(dispatch.bind(args())); 74 | } 75 | 76 | public function download(version:String) { 77 | 78 | return (switch ((version : UserVersion) : UserVersionData) { 79 | case UNightly(_) | UOfficial(_): 80 | api.resolveInstalled(version); 81 | default: 82 | Promise.lift(new Error('$version needs to be resolved online')); 83 | }).tryRecover(function (_) { 84 | log('Looking up Haxe version "$version" online'); 85 | return api.resolveOnline(version).next(function (r) { 86 | log(' Resolved to $r.'); 87 | return r; 88 | }); 89 | }).next(function (r) { 90 | return api.download(r, { force: force }).next(function (wasDownloaded) { 91 | 92 | log( 93 | if (!wasDownloaded) 94 | ' ... already downloaded!' 95 | else 96 | '' 97 | ); 98 | 99 | return r; 100 | }); 101 | }); 102 | } 103 | 104 | function log(s:String) 105 | if (!api.silent) Sys.println(s); 106 | 107 | public function switchTo(version:ResolvedVersion) 108 | return api.switchTo(version).next(function (v) { 109 | log('Now using $version'); 110 | return v; 111 | }); 112 | 113 | public function makeCommands() { 114 | var scope = api.scope; 115 | 116 | return [ 117 | new Command('install', '[]', 'installs the version if specified, otherwise\ninstalls the currently configured version', 118 | function (args) return switch args { 119 | case [v]: 120 | download(v).next(switchTo); 121 | case []: 122 | download(scope.config.version).next(switchTo); 123 | case v: 124 | new Error('command `install` accepts one argument at most (i.e. the version)'); 125 | } 126 | ), 127 | new Command('download', '', 'downloads the specified version', 128 | function (args) return switch args { 129 | case [v]: download(v); 130 | case []: new Error('not enough arguments'); 131 | case v: new Error('too many arguments'); 132 | } 133 | ), 134 | new Command('use', '', 'switches to the specified version', 135 | function (args) return switch args { 136 | case [v]: api.resolveInstalled(v).next(switchTo); 137 | case []: new Error('not enough arguments'); 138 | case v: new Error('too many arguments'); 139 | } 140 | ), 141 | new Command('scope', '[create|delete|set]\n[scoped|mixed|haxelib]', 'creates, deletes or configures\nthe current scope or inspects it\nif no argument is supplied', 142 | function (args) return switch args[0] { 143 | case 'set': 144 | switch args.slice(1) { 145 | case []: new Error('not enough arguments'); 146 | case [v]: 147 | 148 | LibResolution.parse(v).map(function (v) { 149 | scope.reconfigure({ 150 | version: scope.config.version, 151 | resolveLibs: v 152 | }); 153 | return Noise; 154 | }); 155 | 156 | case v: new Error('too many arguments'); 157 | } 158 | case 'create': 159 | Promise.lift(switch args.slice(1) { 160 | case []: if (scope.isGlobal) Scoped else scope.config.resolveLibs; 161 | case [v]: LibResolution.parse(v); 162 | default: new Error('too many arguments'); 163 | }).next(function (resolution) return { 164 | Scope.create(scope.cwd, { 165 | version: scope.config.version, 166 | resolveLibs: if (scope.isGlobal) Scoped else scope.config.resolveLibs, 167 | }); 168 | return Noise; 169 | }); 170 | case 'delete': 171 | if (scope.isGlobal) 172 | new Error('Cannot delete global scope'); 173 | else { 174 | scope.delete(); 175 | log('deleted scope in ${scope.scopeDir}'); 176 | Noise; 177 | } 178 | case null: 179 | println( 180 | (if (scope.isGlobal) '[global]' 181 | else '[local]') + ' ${scope.scopeDir}' 182 | ); 183 | Noise; 184 | case v: 185 | new Error('Invalid arguments'); 186 | } 187 | ), 188 | new Command('list', '', 'lists currently downloaded versions', 189 | function (args) return switch args { 190 | case []: 191 | api.officialInstalled(IncludePrereleases).next(function (o) { 192 | return api.nightliesInstalled().next(function (n) { 193 | function highlight(s:String) 194 | return 195 | if (s == scope.config.version) 196 | ' -> $s'; 197 | else 198 | ' $s'; 199 | 200 | println(''); 201 | println('Using ${(scope.config.version:UserVersion)}'); 202 | println(''); 203 | println('Official releases:'); 204 | println(''); 205 | 206 | for (v in o) 207 | println(highlight(v)); 208 | 209 | if (n.iterator().hasNext()) { 210 | println(''); 211 | println('Nightly builds:'); 212 | println(''); 213 | 214 | for (v in n) 215 | println(highlight(v.hash) + v.published.format(' (%Y-%m-%d %H:%M)')); 216 | } 217 | 218 | println(''); 219 | 220 | return Noise; 221 | }); 222 | }); 223 | default: 224 | new Error('command `list` does expect arguments'); 225 | } 226 | ) 227 | ]; 228 | } 229 | 230 | static function dispatch(args:Array, ?cb) { 231 | 232 | var scope = Scope.seek({ cwd: if (args.remove('--global')) Scope.DEFAULT_ROOT else null }); 233 | 234 | var cli = new Cli(new Switchx(scope, args.remove('--silent')), args.remove('--force')); 235 | 236 | Command.dispatch(args, 'switchx - haxe version switcher', cli.makeCommands(), [ 237 | new Named('Supported switches', [ 238 | new Named('--silent', 'disables logging'), 239 | new Named('--global', 'performs operation on global scope'), 240 | new Named('--force', 'forces re-download'), 241 | ]), 242 | ALIASES, 243 | ]).handle(function (o) { 244 | Command.reportOutcome(o); 245 | if (cb != null) cb(); 246 | }); 247 | } 248 | 249 | static public var ALIASES = new Named('Version aliases', [ 250 | new Named('edge, nightly', 'latest nightly build from builds.haxe.org'), 251 | new Named('latest', 'latest official release from haxe.org'), 252 | new Named('stable', 'latest stable release from haxe.org'), 253 | ]); 254 | } 255 | -------------------------------------------------------------------------------- /src/switchx/Switchx.hx: -------------------------------------------------------------------------------- 1 | package switchx; 2 | 3 | import haxe.io.Bytes; 4 | 5 | using sys.io.File; 6 | using haxe.io.Path; 7 | using switchx.Version; 8 | using tink.CoreApi; 9 | using DateTools; 10 | using StringTools; 11 | using sys.FileSystem; 12 | using haxe.Json; 13 | using switchx.Fs; 14 | 15 | enum PickOfficial { 16 | StableOnly; 17 | IncludePrereleases; 18 | } 19 | 20 | class Switchx { 21 | 22 | public var scope(default, null):haxeshim.Scope; 23 | public var silent(default, null):Bool; 24 | var downloads:String; 25 | 26 | public function new(scope, silent) { 27 | this.scope = scope; 28 | this.silent = silent; 29 | 30 | Fs.ensureDir(scope.versionDir.addTrailingSlash()); 31 | Fs.ensureDir(scope.haxelibRepo.addTrailingSlash()); 32 | Fs.ensureDir(this.downloads = scope.haxeshimRoot + '/downloads/'); 33 | } 34 | 35 | static var VERSION_INFO = 'version.json'; 36 | static var NIGHTLIES = 'http://hxbuilds.s3-website-us-east-1.amazonaws.com/builds/haxe'; 37 | static var PLATFORM = 38 | switch Sys.systemName() { 39 | case 'Windows': 'windows'; 40 | case 'Mac': 'mac'; 41 | default: 'linux64'; 42 | } 43 | 44 | static function linkToNightly(hash:String, date:Date) { 45 | var extension = 46 | // Windows builds are distributed as a zip file since 2017-05-11 47 | if (PLATFORM == 'windows' && date.getTime() > 1494460800000) 'zip' 48 | else 'tar.gz'; 49 | return date.format('$NIGHTLIES/$PLATFORM/haxe_%Y-%m-%d_development_$hash.$extension'); 50 | } 51 | 52 | static function sortedOfficial(kind:PickOfficial, versions:Array):Iterable { 53 | if (kind == StableOnly) 54 | versions = [for (v in versions) if (!v.isPrerelease) v]; 55 | versions.sort(Official.compare); 56 | return versions; 57 | } 58 | 59 | static public function officialOnline(kind:PickOfficial):Promise> 60 | return Download.text('https://raw.githubusercontent.com/HaxeFoundation/haxe.org/staging/downloads/versions.json') 61 | .next(function (s) { 62 | return sortedOfficial(kind, s.parse().versions.map(function (v) return v.version)); 63 | }); 64 | 65 | static function sortedNightlies(raw:Array):Iterable { 66 | raw.sort(function (a, b) return Reflect.compare(b.published.getTime(), a.published.getTime())); 67 | return raw; 68 | } 69 | 70 | static public function nightliesOnline():Promise> { 71 | return Download.text('$NIGHTLIES/$PLATFORM/').next(function (s:String):Iterable { 72 | var lines = s.split('------------------\n').pop().split('\n'); 73 | var ret = []; 74 | for (l in lines) 75 | switch l.trim() { 76 | case '': 77 | case v: 78 | if (v.indexOf('_development_') != -1) 79 | switch v.indexOf(' ') { 80 | case -1: //whatever 81 | case v.substr(0, _).split(' ') => [ 82 | _.split('-').map(Std.parseInt) => [y, m, d], 83 | _.split(':').map(Std.parseInt) => [hh, mm, ss] 84 | ]: 85 | 86 | ret.push({ 87 | hash: v.split('_development_').pop().split('.').shift(), 88 | published: new Date(y, m - 1, d, hh, mm, ss), 89 | }); 90 | 91 | default: 92 | 93 | } 94 | 95 | } 96 | return sortedNightlies(ret); 97 | }); 98 | } 99 | 100 | public function officialInstalled(kind):Promise> 101 | return 102 | attempt( 103 | 'Get installed Haxe versions', 104 | sortedOfficial(kind, [for (v in scope.versionDir.readDirectory()) 105 | if (!v.isHash() && versionDir(v).isDirectory()) v 106 | ]) 107 | ); 108 | 109 | static function attempt(what:String, l:Lazy):Promise 110 | return 111 | try 112 | Success(l.get()) 113 | catch (e:Dynamic) 114 | Failure(new Error('Failed to $what because $e')); 115 | 116 | public function nightliesInstalled() 117 | return 118 | attempt( 119 | 'get installed Haxe versions', 120 | sortedNightlies([for (v in scope.versionDir.readDirectory().filter(UserVersion.isHash)) { 121 | hash:v, 122 | published: Date.fromString('${versionDir(v)}/$VERSION_INFO'.getContent().parse().published) 123 | }]) 124 | ); 125 | 126 | public function switchTo(version:ResolvedVersion):Promise 127 | return attempt('save new configuration to ${scope.configFile}', function () { 128 | scope.reconfigure({ 129 | version: version.id, 130 | resolveLibs: scope.config.resolveLibs, 131 | }); 132 | 133 | return Noise; 134 | }); 135 | 136 | public function resolveInstalled(version:UserVersion):Promise 137 | return resolve(version, officialInstalled, nightliesInstalled); 138 | 139 | public function resolveOnline(version:UserVersion):Promise 140 | return resolve(version, officialOnline, nightliesOnline); 141 | 142 | static function pickFirst(kind:String, make:A->ResolvedVersion):Next, ResolvedVersion> 143 | return function (i:Iterable) 144 | return switch i.iterator().next() { 145 | case null: new Error(NotFound, 'No $kind build found'); 146 | case v: make(v); 147 | } 148 | 149 | function resolve(version:UserVersion, getOfficial:PickOfficial->Promise>, getNightlies:Void->Promise>):Promise 150 | return switch version { 151 | case UEdge: 152 | 153 | getNightlies().next(pickFirst('nightly', RNightly)); 154 | 155 | case ULatest: 156 | 157 | getOfficial(IncludePrereleases).next(pickFirst('official', ROfficial)); 158 | 159 | case UStable: 160 | 161 | getOfficial(StableOnly).next(pickFirst('stable', ROfficial)); 162 | 163 | case UNightly(hash): 164 | 165 | getNightlies().next(function (v) { 166 | for (n in v) 167 | if (n.hash == hash) 168 | return RNightly(n); 169 | 170 | return new Error(NotFound, 'Unknown nightly $version'); 171 | }); 172 | 173 | case UOfficial(version): 174 | 175 | getOfficial(IncludePrereleases).next(function (versions) 176 | return 177 | if (Lambda.has(versions, version)) ROfficial(version) 178 | else new Error(NotFound, 'Unknown version $version') 179 | ); 180 | 181 | case UCustom(path): RCustom(path); 182 | } 183 | 184 | function versionDir(name:String) 185 | return scope.getInstallation(name).path; 186 | 187 | function isDownloaded(r:ResolvedVersion) 188 | return versionDir(r.id).exists(); 189 | 190 | function linkToOfficial(version) 191 | return 192 | 'http://haxe.org/website-content/downloads/$version/downloads/haxe-$version-' + switch Sys.systemName() { 193 | case 'Windows': 'win.zip'; 194 | case 'Mac': 'osx.tar.gz'; 195 | default: 196 | if (version < "3") 197 | 'linux.tar.gz'; 198 | else 199 | 'linux64.tar.gz'; 200 | } 201 | 202 | function replace(target:String, replacement:String, archiveAs:String, ?beforeReplace) { 203 | var root = replacement; 204 | 205 | while (true) 206 | switch replacement.ls() { 207 | case [sub]: 208 | replacement = sub; 209 | default: break; 210 | } 211 | 212 | if (beforeReplace != null) 213 | beforeReplace(replacement); 214 | 215 | if (target.exists()) { 216 | var old = '$downloads/$archiveAs@${Math.floor(target.stat().ctime.getTime())}'; 217 | target.rename(old); 218 | replacement.rename(target); 219 | } 220 | else { 221 | replacement.rename(target); 222 | } 223 | 224 | if (root.exists()) 225 | root.delete(); 226 | } 227 | 228 | public function download(version:ResolvedVersion, options:{ force: Bool }):Promise { 229 | 230 | inline function download(url, into) 231 | return Download.archive(url, 0, into, !silent); 232 | 233 | return switch version { 234 | case RCustom(_): 235 | 236 | new Error('Cannot download custom version'); 237 | 238 | case isDownloaded(_) => true if (options.force != true): 239 | 240 | false; 241 | 242 | case RNightly({ hash: hash, published: date }): 243 | 244 | download(linkToNightly(hash, date), '$downloads/$hash@${Math.floor(Date.now().getTime())}').next(function (dir) { 245 | replace(versionDir(hash), dir, hash, function (dir) { 246 | '$dir/$VERSION_INFO'.saveContent(haxe.Json.stringify({ 247 | published: date.toString(), 248 | })); 249 | }); 250 | return true; 251 | }); 252 | 253 | case ROfficial(version): 254 | 255 | var url = linkToOfficial(version), 256 | tmp = '$downloads/$version@${Math.floor(Date.now().getTime())}'; 257 | 258 | var ret = download(url, tmp); 259 | 260 | ret.next(function (v) { 261 | replace(versionDir(version), v, version); 262 | return true; 263 | }); 264 | } 265 | } 266 | 267 | } -------------------------------------------------------------------------------- /src/switchx/Download.hx: -------------------------------------------------------------------------------- 1 | package switchx; 2 | 3 | import haxe.Timer; 4 | import haxe.io.*; 5 | 6 | import haxeshim.node.*; 7 | 8 | import js.node.Buffer; 9 | import js.node.Url; 10 | import js.node.Http; 11 | import js.Node.*; 12 | import js.node.stream.Readable.IReadable; 13 | import js.node.http.ClientRequest; 14 | import js.node.http.IncomingMessage; 15 | 16 | //using js.node.Readline; 17 | using tink.CoreApi; 18 | using StringTools; 19 | 20 | typedef Directory = String; 21 | 22 | private typedef Events = { 23 | function onProgress(loaded:Int, total:Int, binary:Bool):Void; 24 | function done(result:Outcome):Void; 25 | } 26 | 27 | private typedef ProgressHandler = String->IncomingMessage->Events->Void; 28 | private typedef Handler = String->IncomingMessage->(Outcome->Void)->Void; 29 | 30 | class Download { 31 | 32 | static public function text(url:String):Promise 33 | return bytes(url).next(function (b) return b.toString()); 34 | 35 | static public function bytes(url:String):Promise 36 | return download(url, function (_, r, cb) buffered(r).handle(cb)); 37 | 38 | static function buffered(r:IncomingMessage):Promise 39 | return Future.async(function (cb) { 40 | var ret = []; 41 | r.on('data', ret.push); 42 | r.on('end', function () { 43 | cb(Success(Buffer.concat(ret).hxToBytes())); 44 | }); 45 | }); 46 | 47 | static public function archive(url:String, peel:Int, into:String, ?progress:Bool) { 48 | return download(url, withProgress(progress, function (finalUrl:String, res, events) { 49 | if (res.headers['content-type'] == 'application/zip' || url.endsWith('.zip') || finalUrl.endsWith('.zip')) 50 | unzip(url, into, peel, res, events); 51 | else 52 | untar(url, into, peel, res, events); 53 | })); 54 | } 55 | 56 | static function unzip(src:String, into:String, peel:Int, res:IncomingMessage, events:Events) { 57 | buffered(res).next(function (bytes) 58 | return Future.async(function (cb) { 59 | Yauzl.fromBuffer(Buffer.hxFromBytes(bytes), function (err, zip) { 60 | var saved = -1;//something is really weird about this lib 61 | function done() { 62 | saved += 1; 63 | events.onProgress(saved, zip.entryCount, false); 64 | if (saved == zip.entryCount) 65 | haxe.Timer.delay(cb.bind(Success(into)), 100); 66 | } 67 | 68 | if (err != null) 69 | cb(Failure(new Error(UnprocessableEntity, 'Failed to unzip $src'))); 70 | 71 | zip.on("entry", function (entry) switch Fs.peel(entry.fileName, peel) { 72 | case None: 73 | case Some(f): 74 | //trace([zip.entriesRead, zip.entryCount]); 75 | var path = '$into/$f'; 76 | if (path.endsWith('/')) 77 | done(); 78 | else { 79 | Fs.ensureDir(path); 80 | zip.openReadStream(entry, function (e, stream) { 81 | var out = js.node.Fs.createWriteStream(path); 82 | stream.pipe(out, { end: true } ); 83 | out.on('close', done); 84 | }); 85 | } 86 | 87 | }); 88 | zip.on("end", function () { 89 | zip.close(); 90 | done(); 91 | }); 92 | }); 93 | })).handle(events.done); 94 | } 95 | static public function untar(src:String, into:String, peel:Int, res:IReadable, events:Events) 96 | return Future.async(function (cb) { 97 | var total = 0, 98 | written = 0; 99 | 100 | var symlinks = []; 101 | 102 | function update() 103 | events.onProgress(written, total + 1, true); 104 | 105 | var pending = 1; 106 | function done(progress = 0) { 107 | written += progress; 108 | update(); 109 | haxe.Timer.delay(function () { 110 | if (--pending <= 0) { 111 | events.onProgress(total, total, true); 112 | Promise.inParallel([for (link in symlinks) 113 | Future.async(function (cb) 114 | js.node.Fs.unlink(link.to, function (_) 115 | js.node.Fs.symlink(link.from, link.to, function (e:js.Error) cb(//TODO: figure out if mode needs to be set 116 | if (e == null) Success(Noise) 117 | else Failure(new Error(e.message)) 118 | )) 119 | ) 120 | ) 121 | ]).next(function (_) return into).handle(cb); 122 | } 123 | }, 100); 124 | } 125 | 126 | var error:Error = null; 127 | 128 | function fail(message:String) 129 | cb(Failure(error = new Error(message))); 130 | 131 | Tar.parse(res, function (entry) { 132 | if (error != null) return; 133 | total += entry.size; 134 | update(); 135 | 136 | function skip() { 137 | entry.on('data', function () {}); 138 | } 139 | switch Fs.peel(entry.path, peel) { 140 | case None: 141 | skip(); 142 | case Some(f): 143 | var path = '$into/$f'; 144 | if (path.endsWith('/')) 145 | skip(); 146 | else { 147 | Fs.ensureDir(path); 148 | if (entry.type == SymbolicLink) { 149 | skip(); 150 | symlinks.push({ from: Path.join([Path.directory(path), entry.linkpath]), to: path }); 151 | } 152 | else { 153 | pending++; 154 | var buffer = @:privateAccess new js.node.stream.PassThrough(); 155 | var out = js.node.Fs.createWriteStream(path, { mode: entry.mode }); 156 | entry.pipe(buffer, { end: true } ); 157 | buffer.pipe(out, { end: true } ); 158 | out.on('close', done.bind(entry.size)); 159 | } 160 | } 161 | } 162 | }).handle(function (o) switch o { 163 | case Failure(e): cb(Failure(e)); 164 | default: done(); 165 | }); 166 | }).handle(events.done); 167 | 168 | static public function tar(url:String, peel:Int, into:String, ?progress:Bool):Promise 169 | return download(url, withProgress(progress, untar.bind(_, into, peel))); 170 | 171 | 172 | static public function zip(url:String, peel:Int, into:String, ?progress:Bool):Promise 173 | return download(url, withProgress(progress, unzip.bind(_, into, peel))); 174 | 175 | static function withProgress(?progress:Bool, handler:ProgressHandler):Handler { 176 | return 177 | function (url:String, msg:IncomingMessage, cb:Outcome->Void) { 178 | if (progress != true || !process.stdout.isTTY) { 179 | handler(url, msg, { 180 | onProgress: function (_, _, _) {}, 181 | done: cb, 182 | }); 183 | return; 184 | } 185 | 186 | var size = Std.parseInt(msg.headers.get('content-length')), 187 | loaded = 0, 188 | saved = 0, 189 | total = 1; 190 | 191 | var last = null; 192 | 193 | function progress(s:String) { 194 | if (s == last) return; 195 | last = s; 196 | untyped { 197 | process.stdout.clearLine(0); 198 | process.stdout.cursorTo(0); 199 | } 200 | process.stdout.write(s); 201 | } 202 | 203 | function pct(f:Float) 204 | return (switch Std.string(Math.round(1000 * f) / 10) { 205 | case whole = _.indexOf('.') => -1: '$whole.0'; 206 | case v: v; 207 | }).lpad(' ', 5) + '%'; 208 | 209 | var lastUpdate = Date.fromTime(0).getTime(); 210 | 211 | function update() { 212 | if (saved == total || total == 0) progress('Done!\n'); 213 | else { 214 | var now = Date.now().getTime(); 215 | if (now > lastUpdate + 137) { 216 | 217 | lastUpdate = now; 218 | var messages = []; 219 | if (loaded < size) messages.push('Downloaded: ${pct(loaded / size)}'); 220 | if (saved > 0) messages.push('Saved: ${pct(saved / total)}'); 221 | progress(messages.join(' ')); 222 | } 223 | } 224 | } 225 | 226 | msg.on('data', function (buf) { 227 | loaded += buf.length; 228 | update(); 229 | }); 230 | 231 | var last = .0; 232 | handler(url, msg, { 233 | onProgress: function (_saved, _total, binary) { 234 | saved = _saved; 235 | total = _total; 236 | /** 237 | The following is truly hideous, but there's no easy way 238 | to actually KNOW how much of a .tar.gz you have unpacked, because apparently: 239 | 240 | - tar was made for freaking TAPE WRITERS and is just a stream of entries, with no real 241 | beginning where the number of files might be mentioned (see https://en.wikipedia.org/wiki/Tar_(computing)#Random_access) 242 | - gzip doesn't seem to give any hints as to how big the file should be when uncompressed 243 | **/ 244 | if (binary) { 245 | var downloaded = loaded / size; 246 | var decompressed = saved / total; 247 | var estimate = downloaded * decompressed; 248 | if (estimate < last) 249 | estimate = last; 250 | last = estimate; 251 | saved = Math.round(estimate * 1000); 252 | total = 1000; 253 | } 254 | update(); 255 | }, 256 | done: cb, 257 | }); 258 | } 259 | } 260 | 261 | static function download(url:String, handler:Handler):Promise 262 | return Future.async(function (cb) { 263 | 264 | var options:HttpRequestOptions = cast Url.parse(url); 265 | 266 | options.agent = false; 267 | if (options.headers == null) 268 | options.headers = {}; 269 | options.headers['user-agent'] = Download.USER_AGENT; 270 | 271 | function fail(e:js.Error) 272 | cb(Failure(tink.core.Error.withData('Failed to download $url because ${e.message}', e))); 273 | 274 | var req = 275 | if (url.startsWith('https:')) js.node.Https.get(cast options); 276 | else js.node.Http.get(options); 277 | 278 | req.setTimeout(30000); 279 | req.on('error', fail); 280 | 281 | req.on(ClientRequestEvent.Response, function (res) { 282 | if (res.statusCode >= 400) 283 | cb(Failure(Error.withData(res.statusCode, res.statusMessage, res))); 284 | else 285 | switch res.headers['location'] { 286 | case null: 287 | res.on('error', fail); 288 | 289 | handler(url, res, function (v) { 290 | switch v { 291 | case Success(x): cb(Success(x)); 292 | case Failure(e): cb(Failure(e)); 293 | } 294 | }); 295 | case v: 296 | 297 | download(switch Url.parse(v) { 298 | case { protocol: null }: 299 | options.protocol + '//' + options.host + v; 300 | default: v; 301 | }, handler).handle(cb); 302 | } 303 | }); 304 | }); 305 | 306 | static public var USER_AGENT = 'switchx'; 307 | } 308 | --------------------------------------------------------------------------------