1 /** 2 This package holds function for easy verification of external tools' 3 existence. 4 5 Copyright: © 2019 Arne Ludwig <arne.ludwig@posteo.de> 6 License: Subject to the terms of the MIT license, as written in the 7 included LICENSE file. 8 Authors: Arne Ludwig <arne.ludwig@posteo.de> 9 */ 10 module dalicious.dependency; 11 12 13 /** 14 This struct can be used as a decorator to mark an external dependency of 15 any symbol. 16 */ 17 struct ExternalDependency 18 { 19 /// Name of the executable 20 string executable; 21 /// Name of the package that provides the executable. 22 string package_; 23 /// URL to the homepage of the package. 24 string url; 25 26 /// Get a human-readable string describing this dependency. 27 string toString() const pure nothrow 28 { 29 if (package_ is null && url is null) 30 return executable; 31 else if (url is null) 32 return executable ~ " (part of `" ~ package_ ~ "`)"; 33 else if (package_ is null) 34 return executable ~ " (see " ~ url ~ ")"; 35 else 36 return executable ~ " (part of `" ~ package_ ~ "`; see " ~ url ~ ")"; 37 } 38 } 39 40 // not for public use 41 static enum isExternalDependency(alias value) = is(typeof(value) == ExternalDependency); 42 43 unittest 44 { 45 static assert(isExternalDependency!(ExternalDependency("someTool"))); 46 static assert(!isExternalDependency!"someTool"); 47 } 48 49 import std.meta : Filter, staticMap; 50 import std.traits : getUDAs; 51 52 // not for public use 53 static enum ExternalDependency[] fromSymbol(alias symbol) = [Filter!( 54 isExternalDependency, 55 getUDAs!(symbol, ExternalDependency), 56 )]; 57 58 unittest 59 { 60 @ExternalDependency("someTool") 61 void callSomeTool(in string parameter) 62 { 63 // calls `someTool` 64 } 65 66 static assert(fromSymbol!callSomeTool == [ExternalDependency("someTool")]); 67 } 68 69 /** 70 Generates an array of external dependencies of `Modules`. 71 72 Params: 73 Modules = List of symbols to be checked. 74 */ 75 ExternalDependency[] externalDependencies(Modules...)() 76 { 77 static assert(Modules.length > 0, "missing Modules"); 78 79 import std.array : array; 80 import std.algorithm : 81 joiner, 82 sort, 83 uniq; 84 import std.meta : staticMap; 85 import std.traits : getSymbolsByUDA; 86 87 alias _getSymbolsByUDA(alias Module) = getSymbolsByUDA!(Module, ExternalDependency); 88 alias byExecutableLt = (a, b) => a.executable < b.executable; 89 alias byExecutableEq = (a, b) => a.executable == b.executable; 90 91 ExternalDependency[][] deps = [staticMap!( 92 fromSymbol, 93 staticMap!( 94 _getSymbolsByUDA, 95 Modules, 96 ), 97 )]; 98 99 return deps.joiner.array.sort!byExecutableLt.release.uniq!byExecutableEq.array; 100 } 101 102 /// 103 unittest 104 { 105 struct Caller 106 { 107 @ExternalDependency("someTool") 108 void callSomeTool(in string parameter) 109 { 110 // calls `someTool` 111 } 112 113 @ExternalDependency("otherTool") 114 void callOtherTool(in string parameter) 115 { 116 // calls `otherTool` 117 } 118 } 119 120 static assert(externalDependencies!Caller == [ 121 ExternalDependency("otherTool"), 122 ExternalDependency("someTool"), 123 ]); 124 } 125 126 unittest 127 { 128 static assert(!is(externalDependencies)); 129 } 130 131 /** 132 Generates an array of external dependencies of `Modules`. 133 134 Params: 135 Modules = List of symbols to be checked. 136 */ 137 version(Posix) void enforceExternalDepencenciesAvailable(Modules...)() 138 { 139 import dalicious.process : isExecutable; 140 import std.array : array; 141 import std.algorithm : filter; 142 import std.exception : enforce; 143 import std.format : format; 144 import std.process : execute; 145 import std.range : enumerate; 146 import std.string : lineSplitter; 147 148 enum modulesDeps = externalDependencies!Modules; 149 150 static if (modulesDeps.length > 0) 151 { 152 auto missingExternalTools = modulesDeps 153 .filter!(extDep => !isExecutable(extDep.executable)) 154 .array; 155 156 if (missingExternalTools.length > 0) 157 throw new ExternalDependencyMissing(missingExternalTools); 158 } 159 else 160 { 161 pragma(msg, "Info: your program has no external dependencies but checks for their availability."); 162 } 163 } 164 165 /// ditto 166 version(Posix) deprecated alias enforceExternalToolsAvailable = enforceExternalDepencenciesAvailable; 167 168 /// 169 unittest 170 { 171 import std.exception : assertThrown; 172 173 struct Caller 174 { 175 @ExternalDependency("/this/is/missing") 176 @ExternalDependency("this_too_is_missing", "mypack", "http://example.com/") 177 void makeCall() { } 178 } 179 180 assertThrown!ExternalDependencyMissing(enforceExternalDepencenciesAvailable!Caller()); 181 // Error message: 182 // 183 // missing external tools: 184 // - /this/is/missing 185 // - this_too_is_missing (part of `mypack`; see http://example.com/) 186 // 187 // Check your PATH and/or install the required software. 188 } 189 190 191 unittest 192 { 193 import std.exception : assertThrown; 194 195 enum dependencies = [ 196 ExternalDependency("/bin/sh"), 197 ExternalDependency("sh"), 198 ExternalDependency("this_too_is_missing", "mypack", "http://example.com/"), 199 ]; 200 201 struct Caller 202 { 203 @(dependencies[0]) 204 @(dependencies[1]) 205 void makeCall() { } 206 } 207 208 try 209 { 210 enforceExternalDepencenciesAvailable!Caller(); 211 } 212 catch (ExternalDependencyMissing e) 213 { 214 assert(e.missingExternalTools[0] == dependencies[$ - 1]); 215 } 216 } 217 218 219 unittest 220 { 221 import std.exception : assertThrown; 222 import std.algorithm : map; 223 import std.array : array; 224 import std.format : format; 225 import std.range : iota; 226 227 enum dependencies = iota(100) 228 .map!(i => ExternalDependency(format!"this_is_missing_%d"(i))) 229 .array; 230 231 struct Caller 232 { 233 mixin(format!q{ 234 %-(@(dependencies[%d])%)]) 235 void makeCall() { } 236 }(iota(dependencies.length))); 237 } 238 239 try 240 { 241 enforceExternalDepencenciesAvailable!Caller(); 242 } 243 catch (ExternalDependencyMissing e) 244 { 245 assert(e.missingExternalTools.length == dependencies.length); 246 } 247 } 248 249 250 /// Thrown if one or more external dependencies are missing. 251 class ExternalDependencyMissing : Exception 252 { 253 static enum errorMessage = "missing external tools:\n%-(- %s\n%)\n\nCheck your PATH and/or install the required software."; 254 255 /// List of missing external depencencies. 256 const(ExternalDependency[]) missingExternalTools; 257 258 /** 259 Params: 260 missingExternalTools = List of missing external tools 261 file = The file where the exception occurred. 262 line = The line number where the exception 263 occurred. 264 next = The previous exception in the chain of 265 exceptions, if any. 266 */ 267 this(const(ExternalDependency[]) missingExternalTools, string file = __FILE__, size_t line = __LINE__, 268 Throwable next = null) pure 269 { 270 import std.format : format; 271 272 super(format!errorMessage(missingExternalTools), file, line, next); 273 this.missingExternalTools = missingExternalTools; 274 } 275 }