package runci; import haxe.Timer; import sys.io.Process; import runci.Config.*; using StringTools; class CommandFailure extends haxe.Exception { public final exitCode:Int; public function new(exitCode:Int = 1) { super("Command failed: " + Std.string(exitCode)); this.exitCode = exitCode; } } class System { static public function successMsg(msg:String):Void { Sys.println(colorSupported ? '\x1b[32m' + msg + '\x1b[0m' : msg); } static public function failMsg(msg:String):Void { Sys.println(colorSupported ? '\x1b[31m' + msg + '\x1b[0m' : msg); } static public function infoMsg(msg:String):Void { Sys.println(colorSupported ? '\x1b[36m' + msg + '\x1b[0m' : msg); } static public function commandSucceed(cmd:String, args:Array):Bool { return try { final p = new Process(cmd, args); final succeed = p.exitCode() == 0; p.close(); succeed; } catch(e:Dynamic) false; } static public function commandResult(cmd:String, args:Array):{ stdout:String, stderr:String, exitCode:Int } { final p = new Process(cmd, args); final out = { stdout: p.stdout.readAll().toString(), stderr: p.stderr.readAll().toString(), exitCode: p.exitCode() } p.close(); return out; } static inline function getDisplayCmd(cmd:String, ?args:Array) { return cmd + (args == null ? '' : ' $args'); } /** Run a command using `Sys.command()`. If the command exits with non-zero code, throws `CommandFailure` with the same code. */ static public function runCommand(cmd:String, ?args:Array):Void { final exitCode = showAndRunCommand(cmd, args); if (exitCode != 0) throw new CommandFailure(exitCode); } /** Runs command using `Sys.command()` but ignores failures. **/ static public function attemptCommand(cmd:String, ?args:Array) { showAndRunCommand(cmd, args); } static function showAndRunCommand(cmd:String, args:Null>, ?displayed:String):Int { if (displayed == null) displayed = getDisplayCmd(cmd, args); infoMsg('Command: $displayed'); final t = Timer.stamp(); final exitCode = Sys.command(cmd, args); final dt = Math.round(Timer.stamp() - t); final msg = 'Command exited with $exitCode in ${dt}s: $displayed'; if (exitCode != 0) failMsg(msg); else successMsg(msg); return exitCode; } static final TRIALS = 3; /** Run a command using `Sys.command()` with up to three attempts. Useful for running network-dependent commands. If all three attempts fail, throws a `CommandFailure` with the exit code of the last run. **/ static public function runNetworkCommand(cmd:String, ?args:Array) { final cmdStr = getDisplayCmd(cmd, args); for (trial in 1...TRIALS+1){ final exitCode = showAndRunCommand(cmd, args, cmdStr); if (exitCode == 0) return; if (trial == TRIALS) throw new CommandFailure(exitCode); infoMsg('Command will be re-run...'); } } /** * Recursively delete a directory. * @return Int Exit code of a system command executed to perform deletion. */ static public function deleteDirectoryRecursively(dir:String):Int { return switch (Sys.systemName()) { case "Windows": Sys.command("rmdir", ["/S", "/Q", StringTools.replace(sys.FileSystem.fullPath(dir), "/", "\\")]); case _: Sys.command("rm", ["-rf", dir]); } } static public function addToPATH(path:String):Void { infoMsg('Prepending $path to PATH.'); switch (systemName) { case "Windows": Sys.putEnv("PATH", path + ";" + Sys.getEnv("PATH")); case "Mac", "Linux": Sys.putEnv("PATH", path + ":" + Sys.getEnv("PATH")); } } static public function addToLIBPATH(path:String):Void { infoMsg('Prepending $path to loader path.'); switch (systemName) { case "Windows": // pass case "Linux": Sys.putEnv("LD_LIBRARY_PATH", path + ":" + Sys.getEnv("LD_LIBRARY_PATH")); case "Mac": Sys.putEnv("DYLD_LIBRARY_PATH", path + ":" + Sys.getEnv("DYLD_LIBRARY_PATH")); } } static function isLibraryInstalled(library:String):Bool { return new Process("haxelib", ["path", library]).exitCode() == 0; } static public function haxelibInstallGit(account:String, repository:String, ?branch:String, ?srcPath:String, useRetry:Bool = false, ?altName:String):Void { final name = (altName == null) ? repository : altName; // It could only be installed already if we're on a local machine if (!isCi() && isLibraryInstalled(name)) return infoMsg('$name has already been installed.'); final args = ["git", name, 'https://github.com/$account/$repository']; if (branch != null) args.push(branch); if (srcPath != null) args.push(srcPath); if (useRetry) runNetworkCommand("haxelib", args); else runCommand("haxelib", args); } static public function haxelibInstall(library:String):Void { // It could only be installed already if we're on a local machine if (!isCi() && isLibraryInstalled(library)) return infoMsg('$library has already been installed.'); runCommand("haxelib", ["install", library]); } static public function haxelibDev(library:String, path:String):Void { // It could only be installed already if we're on a local machine if (!isCi() && isLibraryInstalled(library)) return infoMsg('$library has already been installed.'); runCommand("haxelib", ["dev", library, path]); } static public function haxelibRun(args:Array, useRetry:Bool = false):Void { final allArgs = ["run"].concat(args); if (useRetry) runNetworkCommand("haxelib", allArgs); else runCommand("haxelib", allArgs); } static public function getHaxelibPath(libName:String) { final proc = new Process("haxelib", ["path", libName]); final code = proc.exitCode(); var result; do { result = proc.stdout.readLine(); if (!result.startsWith("-L")) { break; } } while(true); proc.close(); if (code != 0) { throw 'Failed to get haxelib path ($result)'; } return result; } static public function changeDirectory(path:String) { Sys.println('Changing directory to $path'); Sys.setCwd(path); } static function mergeArgs(cmd:String, args:Array) { return switch (Sys.systemName()) { case "Windows": [StringTools.replace(cmd, "/", "\\")].concat(args).map(haxe.SysTools.quoteWinArg.bind(_, true)).join(" "); case _: [cmd].concat(args).map(haxe.SysTools.quoteUnixArg).join(" "); } } /* command for setting the environment variable to set before sys tests */ static final setCommand = if (Sys.systemName() == "Windows") "set"; else "export"; static final nameAndValue = "EXISTS=1"; /** Prepares environment for system tests and runs `cmd` with `args` **/ static public function runSysTest(cmd:String, ?args:Array) { final toDisplay = getDisplayCmd(setCommand, [nameAndValue]) + ' && ' + getDisplayCmd(cmd, args); if (args != null) cmd = mergeArgs(cmd, args); final fullCmd = '$setCommand $nameAndValue && $cmd'; final exitCode = showAndRunCommand(fullCmd, null, toDisplay); if (exitCode != 0) throw new CommandFailure(exitCode); } static final installPath = if (systemName == "Windows") Sys.getEnv("USERPROFILE") + "/haxe-ci"; else Sys.getEnv("HOME") + "/haxe-ci"; /** Returns path where packages should be installed. **/ public static inline function getInstallPath():String { return installPath; } /** Returns path where downloads should be placed. **/ public static inline function getDownloadPath():String { return installPath + "/downloads"; } }