1 /* 2 * Copyright (c) 2017-2018 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 module init; 24 25 import std.algorithm : sort, canFind, clamp; 26 import std.array : join, split; 27 import std.ascii : newline; 28 import std.conv : ConvException, to; 29 import std.file; 30 import std.json; 31 import std.path : dirSeparator, buildNormalizedPath, absolutePath, relativePath; 32 import std.process : environment, executeShell; 33 import std.regex : matchFirst, ctRegex; 34 import std.stdio : writeln; 35 import std.string; 36 import std.zip; 37 38 import selery.about; 39 40 import toml; 41 import toml.json; 42 43 enum size_t __GENERATOR__ = 49; 44 45 void main(string[] args) { 46 47 string libraries; 48 if(exists(".selery/libraries")) { 49 // should be an absolute normalised path 50 libraries = cast(string)read(".selery/libraries"); 51 } else { 52 // assuming this file is executed in ../ 53 libraries = buildNormalizedPath(absolutePath("..")); 54 } 55 if(!libraries.endsWith(dirSeparator)) libraries ~= dirSeparator; 56 57 bool portable = false; 58 string type = "default"; 59 60 bool plugins = true; 61 62 foreach(arg ; args) { 63 switch(arg.toLower()) { 64 case "--generate-files": 65 mkdirRecurse("views"); 66 write("views/version.txt", Software.displayVersion); 67 string[] notes; 68 string history = cast(string)read("../docs/history.md"); 69 immutable v = "### " ~ Software.displayVersion; 70 auto start = history.indexOf(v); 71 if(start != -1) { 72 start += v.length; 73 history = history[start..$]; 74 immutable end = history.indexOf("##"); 75 write("views/notes.txt", history[0..(end==-1?$:end)].strip.replace("\n", "\\n")); 76 } else { 77 write("views/notes.txt", ""); 78 } 79 JSONValue[string] release; 80 if(environment.get("TRAVIS", "") == "true") { 81 release["ci"] = "travis-ci"; 82 release["repo"] = environment["TRAVIS_REPO_SLUG"]; 83 release["job"] = environment["TRAVIS_JOB_NUMBER"]; 84 } else if(environment.get("APPVEYOR", "").toLower == "true") { 85 release["ci"] = "appveyor"; 86 release["repo"] = environment["APPVEYOR_REPO_NAME"]; 87 release["job"] = environment["APPVEYOR_JOB_NUMBER"]; 88 } 89 write("views/release.json", JSONValue(release).toString()); 90 write("views/is_release.txt", to!string(environment.get("APPVEYOR_REPO_COMMIT_MESSAGE", "").indexOf("[release]") != -1)); 91 return; 92 case "--no-plugins": 93 plugins = false; 94 break; 95 case "--portable": 96 portable = true; 97 break; 98 case "default": 99 case "classic": 100 case "allinone": 101 case "all-in-one": 102 type = "default"; 103 break; 104 case "hub": 105 type = "hub"; 106 break; 107 case "node": 108 type = "node"; 109 break; 110 default: 111 break; 112 } 113 } 114 115 if(portable) { 116 117 auto zip = new ZipArchive(); 118 119 // get all files in assets 120 foreach(string file ; dirEntries("../assets/", SpanMode.breadth)) { 121 immutable name = file[10..$].replace("\\", "/"); 122 if(file.isFile && !name.startsWith(".") && !name.endsWith(".ico") && (!name.startsWith("web/") || name.endsWith("/main.css") || name.indexOf("/res/") != -1)) { 123 //TODO optimise .lang files by removing empty lines, windows endings and comments 124 auto data = read(file); 125 auto member = new ArchiveMember(); 126 member.name = name; 127 member.expandedData(cast(ubyte[])(file.endsWith(".json") ? parseJSON(cast(string)data).toString() : data)); 128 member.compressionMethod = CompressionMethod.deflate; 129 zip.addMember(member); 130 } 131 } 132 mkdirRecurse("views"); 133 write("views/portable.zip", zip.build()); 134 135 } else if(exists("views/portable.zip")) { 136 137 remove("views/portable.zip"); 138 139 } 140 141 TOMLDocument[string] plugs; // plugs[location] = settingsfile 142 143 if(plugins) { 144 145 bool loadPlugin(string path) { 146 if(!path.endsWith(dirSeparator)) path ~= dirSeparator; 147 foreach(pack ; ["plugin.toml", "plugin.json"]) { 148 if(exists(path ~ pack)) { 149 if(pack.endsWith(".toml")) { 150 auto toml = parseTOML(cast(string)read(path ~ pack)); 151 toml["single"] = false; 152 plugs[path] = toml; 153 return true; 154 } else { 155 auto json = parseJSON(cast(string)read(path ~ pack)); 156 if(json.type == JSON_TYPE.OBJECT) { 157 json["single"] = false; 158 plugs[path] = TOMLDocument(toTOML(json).table); 159 return true; 160 } 161 } 162 } 163 } 164 return false; 165 } 166 167 void loadZippedPlugin(string path) { 168 // unzip and load as normal plugin 169 auto data = read(path); 170 auto zip = new ZipArchive(data); 171 immutable name = path[path.lastIndexOf("/")+1..$-4]; 172 immutable dest = ".selery/plugins/" ~ name ~ "/"; 173 bool update = true; 174 if(exists(dest)) { 175 if(exists(dest ~ "crc32.json")) { 176 update = false; 177 auto json = parseJSON(cast(string)read(dest ~ "crc32.json")).object; 178 // compare file names 179 if(sort(json.keys).release() != sort(zip.directory.keys).release()) update = true; 180 else { 181 // compare file's crc32 182 foreach(name, member; zip.directory) { 183 if(member.crc32 != json[name].integer) { 184 update = true; 185 break; 186 } 187 } 188 } 189 } 190 if(update) { 191 foreach(string file ; dirEntries(dest, SpanMode.breadth)) { 192 if(file.isFile) remove(file); 193 } 194 } 195 } else { 196 mkdirRecurse(dest); 197 } 198 if(update) { 199 JSONValue[string] files; 200 foreach(name, member; zip.directory) { 201 files[name] = member.crc32; 202 if(!name.endsWith("/")) { 203 zip.expand(member); 204 if(name.indexOf("/") != -1) mkdirRecurse(dest ~ name[0..name.lastIndexOf("/")]); 205 write(dest ~ name, member.expandedData); 206 } 207 } 208 write(dest ~ "crc32.json", JSONValue(files).toString()); 209 } 210 if(!loadPlugin(dest)) loadPlugin(dest ~ name); 211 } 212 213 void loadSinglePlugin(string location) { 214 immutable name = location[location.lastIndexOf("/")+1..$-2].replace("-", "_"); 215 foreach(line ; split(cast(string)read(location), "\n")) { 216 if(line.strip.startsWith("module") && line[6..$].strip.startsWith(name ~ ";")) { 217 string main = name ~ "."; 218 bool uppercase = true; 219 foreach(c ; name) { 220 if(c == '_') { 221 uppercase = true; 222 } else { 223 if(uppercase) main ~= toUpper("" ~ c); 224 else main ~= c; 225 uppercase = false; 226 } 227 } 228 plugs[location] = TOMLDocument(["name": TOMLValue(name.replace("_", "-")), "main": TOMLValue(main)]); 229 break; 230 } 231 } 232 } 233 234 writeln("Loading plugins for ", Software.name, " ", Software.fullVersion, " configuration \"", type, "\""); 235 236 // load plugins in plugins folder 237 if(exists("../plugins")) { 238 foreach(string ppath ; dirEntries("../plugins/", SpanMode.shallow)) { 239 if(ppath.isDir) { 240 loadPlugin(ppath); 241 } else if(ppath.isFile && ppath.endsWith(".zip")) { 242 loadZippedPlugin(ppath); 243 } else if(ppath.isFile && ppath.endsWith(".d")) { 244 loadSinglePlugin(ppath); 245 } 246 } 247 } 248 249 } 250 251 Info[string] info; 252 253 foreach(path, value; plugs) { 254 Info plugin; 255 plugin.name = value["name"].str; 256 checkName(plugin.name); 257 if(path.isFile) { 258 plugin.single = buildNormalizedPath(absolutePath(path)); 259 } else { 260 if(!path.endsWith(dirSeparator)) path ~= dirSeparator; 261 } 262 if(plugin.name !in info) { 263 plugin.toml = value; 264 plugin.path = buildNormalizedPath(absolutePath(path)); 265 if(!plugin.path.endsWith(dirSeparator)) plugin.path ~= dirSeparator; 266 auto priority = "priority" in value; 267 if(priority) { 268 if(priority.type == TOML_TYPE.STRING) { 269 immutable p = priority.str.toLower; 270 plugin.priority = (p == "high" || p == "🔥") ? 10 : (p == "medium" || p == "normal" ? 5 : 1); 271 } else if(priority.type == TOML_TYPE.INTEGER) { 272 plugin.priority = clamp(priority.integer.to!size_t, 1, 10); 273 } 274 } 275 auto authors = "authors" in value; 276 auto author = "author" in value; 277 if(authors && authors.type == TOML_TYPE.ARRAY) { 278 foreach(a ; authors.array) { 279 if(a.type == TOML_TYPE.STRING) { 280 plugin.authors ~= a.str; 281 } 282 } 283 } else if(author && author.type == TOML_TYPE.STRING) { 284 plugin.authors = [author.str]; 285 } 286 auto main = "main" in value; 287 if(main && main.type == TOML_TYPE.STRING) { 288 string[] spl = main.str.split("."); 289 if(plugin.single.length) { 290 plugin.mod = spl[0]; 291 plugin.main = main.str; 292 } else { 293 immutable m = main.str.lastIndexOf("."); 294 if(m != -1) { 295 plugin.mod = main.str[0..m]; 296 plugin.main = main.str; 297 } 298 } 299 } 300 if(plugin.single.length) { 301 plugin.version_ = "~single"; 302 } else { 303 foreach(string file ; dirEntries(plugin.path ~ "src", SpanMode.breadth)) { 304 if(file.isFile && file.endsWith(dirSeparator ~ "api.d")) { 305 plugin.api = true; 306 break; 307 } 308 } 309 } 310 info[plugin.name] = plugin; 311 } else { 312 throw new Exception("Plugin '" ~ plugin.name ~ " at " ~ plugin.path ~ " conflicts with a plugin with the same name at " ~ info[plugin.name].path); 313 } 314 } 315 316 auto ordered = info.values; 317 318 // sort by priority (or alphabetically) 319 sort!"a.priority == b.priority ? a.name < b.name : a.priority > b.priority"(ordered); 320 321 // control api version 322 foreach(ref inf ; ordered) { 323 if(inf.active) { 324 long[] api; 325 auto ptr = "api" in inf.toml; 326 if(ptr) { 327 if((*ptr).type == TOML_TYPE.INTEGER) { 328 api ~= (*ptr).integer; 329 } else if((*ptr).type == TOML_TYPE.ARRAY) { 330 foreach(v ; (*ptr).array) { 331 if(v.type == TOML_TYPE.INTEGER) api ~= v.integer; 332 } 333 } else if((*ptr).type == TOML_TYPE.TABLE) { 334 auto from = "from" in *ptr; 335 auto to = "to" in *ptr; 336 if(from && (*from).type == TOML_TYPE.INTEGER && to && (*to).type == TOML_TYPE.INTEGER) { 337 foreach(a ; (*from).integer..(*to).integer+1) { 338 api ~= a; 339 } 340 } 341 } 342 } 343 if(api.length == 0 || api.canFind(Software.api)) { 344 writeln(inf.name, " ", inf.version_, ": loaded"); 345 } else { 346 writeln(inf.name, " ", inf.version_, ": cannot load due to wrong api ", api); 347 inf.active = false; 348 } 349 } 350 } 351 352 JSONValue[string] builder; 353 builder["name"] = "selery-builder"; 354 builder["targetType"] = "executable"; 355 builder["targetName"] = (type == "default" ? "selery" : ("selery-" ~ type)) ~ (portable ? "-" ~ Software.displayVersion : ""); 356 builder["targetPath"] = ".."; 357 builder["workingDirectory"] = ".."; 358 builder["sourceFiles"] = ["main/" ~ type ~ ".d", ".selery/builder.d"]; 359 builder["configurations"] = [["name": type]]; 360 builder["dependencies"] = [ 361 "selery": ["path": ".."], 362 "arsd-official:terminal": ["version": "~>1.2.2"], // bug in dub 363 "toml": ["version": "~>0.4.0-rc.4"], 364 "toml:json": ["version": "~>0.4.0-rc.4"], 365 ]; 366 builder["subPackages"] = new JSONValue[0]; 367 368 size_t count = 0; 369 370 string imports = ""; 371 string loads = ""; 372 373 string[] fimports; 374 375 if(!exists(".selery")) mkdir(".selery"); 376 377 foreach(ref value ; ordered) { 378 if(value.active) { 379 count++; 380 if(value.single.length) { 381 builder["sourceFiles"].array ~= JSONValue(relativePath(value.single)); 382 } else { 383 JSONValue[string] sub; 384 sub["name"] = value.name; 385 sub["targetType"] = "library"; 386 sub["targetPath"] = ".." ~ dirSeparator ~ "libs"; 387 sub["configurations"] = [["name": "plugin"]]; 388 sub["dependencies"] = ["selery": ["path": ".."], "arsd-official:terminal": ["version": "~>1.2.2"]], // bug in dub; 389 sub["sourcePaths"] = [relativePath(value.path ~ "src")]; 390 sub["importPaths"] = [relativePath(value.path ~ "src")]; 391 auto dptr = "dependencies" in value.toml; 392 if(dptr && dptr.type == TOML_TYPE.TABLE) { 393 foreach(name, d; dptr.table) { 394 if(name.startsWith("dub:")) { 395 sub["dependencies"][name[4..$]] = toJSON(d); 396 } else { 397 //TODO depends on another plugin 398 } 399 } 400 } 401 builder["subPackages"].array ~= JSONValue(sub); 402 builder["dependencies"][":" ~ value.name] = "*"; 403 } 404 string extra(string path) { 405 auto ret = value.path ~ path; 406 if((value.main.length || value.api) && exists(ret) && ret.isDir) { 407 foreach(f ; dirEntries(ret, SpanMode.breadth)) { 408 // at least one element inside 409 if(f.isFile) return "`" ~ buildNormalizedPath(absolutePath(ret)) ~ dirSeparator ~ "`"; 410 } 411 } 412 return "null"; 413 } 414 if(value.main.length) { 415 imports ~= "static import " ~ value.mod ~ ";\n"; 416 } 417 string load = "ret ~= new PluginOf!(" ~ (value.main.length ? value.main : "Object") ~ ")(`" ~ value.name ~ "`, " ~ value.authors.to!string ~ ", `" ~ value.version_ ~ "`, " ~ to!string(value.api) ~ ", " ~ extra("lang") ~ ", " ~ extra("textures") ~ ");"; 418 auto conditions = "conditions" in value.toml; 419 if(conditions && conditions.type == TOML_TYPE.TABLE) { 420 string[] conds; 421 foreach(key, value; conditions.table) { 422 if(value.type == TOML_TYPE.BOOL) conds ~= "cond!(`" ~ key ~ "`, is_node)(config, " ~ to!string(value.boolean) ~ ")"; 423 } 424 load = "if(" ~ conds.join("&&") ~ "){ " ~ load ~ " }"; 425 } 426 if(value.main.length) load = "static if(is(" ~ value.main ~ " : T)){ " ~ load ~ " }"; 427 if(value.single.length) load = "static if(is(" ~ value.main ~ " == class)){ " ~ load ~ " }"; 428 loads ~= "\t" ~ load ~ "\n"; 429 } 430 431 } 432 433 writeDiff(".selery/builder.d", "module pluginloader;\n\nimport selery.config : Config;\nimport selery.plugin : Plugin;\n\nimport condition;\n\n" ~ imports ~ "\nPlugin[] loadPlugins(alias PluginOf, T, bool is_node)(inout Config config){\n\tPlugin[] ret;\n" ~ loads ~ "\treturn ret;\n}"); 434 435 writeDiff("dub.json", JSONValue(builder).toString()); 436 437 } 438 439 enum invalid = ["selery", "sel", "toml", "default", "hub", "node", "builder", "condition", "config", "starter", "pluginloader"]; 440 441 void checkName(string name) { 442 void error(string message) { 443 throw new Exception("Cannot load plugin '" ~ name ~ "': " ~ message); 444 } 445 if(name.matchFirst(ctRegex!`[^a-z0-9\-]`)) error("Name contains characters outside the range a-z0-9-"); 446 if(name.length == 0 || name.length > 64) error("Invalid name length: " ~ name.length.to!string ~ " is not between 1 and 64"); 447 if(invalid.canFind(name)) error("Name is reserved"); 448 } 449 450 void writeDiff(string location, const void[] data) { 451 if(!exists(location) || read(location) != data) write(location, data); 452 } 453 454 struct Info { 455 456 public TOMLDocument toml; 457 458 public string single; 459 460 public bool active = true; 461 public size_t priority = 1; 462 463 public bool api; 464 465 public string name = ""; 466 public string[] authors = []; 467 public string version_ = "~local"; 468 469 public string path; 470 public string mod; 471 public string main; 472 473 }