1 /++ 2 + Authors: Christian Koestlin 3 + Copyright: Copyright © 2018, Christian Köstlin 4 + License: MIT 5 +/ 6 7 module ponies.dlang; 8 9 import dyaml; 10 import ponies; 11 import std.algorithm; 12 import std.array; 13 import std.conv; 14 import std.experimental.logger; 15 import std.file; 16 import std.regex; 17 import std.stdio; 18 import std..string; 19 import std.traits; 20 21 enum ProtectionLevel 22 { 23 Private, 24 Protected, 25 Public 26 } 27 28 abstract class DlangPony : Pony 29 { 30 protected auto dubSdl = "dub.sdl"; 31 protected auto travisYml = ".travis.yml"; 32 33 override bool applicable() 34 { 35 return exists(dubSdl); 36 } 37 38 protected auto getFromDubSdl(string what) 39 { 40 auto pattern = "^%1$s \"(?P<%1$s>.*)\"$".format(what); 41 auto text = readText(dubSdl); 42 auto match = matchFirst(text, regex(pattern, "m")); 43 return match[what]; 44 } 45 46 protected auto sources() 47 { 48 return dirEntries("source", "*.d", SpanMode.depth); 49 } 50 } 51 52 class DDoxPony : DlangPony 53 { 54 override string name() 55 { 56 return "Setup ddox in %s".format(dubSdl); 57 } 58 59 override CheckStatus check() 60 { 61 try 62 { 63 auto content = readText(dubSdl); 64 return content.canFind("x:ddoxFilterArgs").to!CheckStatus; 65 } 66 catch (FileException e) 67 { 68 return CheckStatus.todo; 69 } 70 } 71 72 override void run() 73 { 74 append(dubSdl, "x:ddoxFilterArgs \"--min-protection=%s\"\n".format(askFor!ProtectionLevel)); 75 } 76 } 77 78 class FormatSourcesPony : DlangPony 79 { 80 override string name() 81 { 82 return "Formats sources with dfmt"; 83 } 84 85 override CheckStatus check() 86 { 87 return CheckStatus.dont_know; 88 } 89 90 override void run() 91 { 92 foreach (string file; sources) 93 { 94 import std.process; 95 96 auto oldContent = readText(file); 97 ["dfmt", "-i", file].execute; 98 auto newContent = readText(file); 99 if (oldContent != newContent) 100 { 101 "FormatSources:%s changed by dfmt -i".format(file).warning; 102 } 103 } 104 } 105 } 106 107 class CopyrightCommentPony : DlangPony 108 { 109 string[] noCopyrightFiles; 110 string copyright; 111 this() 112 { 113 copyright = applicable ? getFromDubSdl("copyright") : null; 114 } 115 116 override string name() 117 { 118 return "Setup copyright headers in .d files (taken from %s)".format(dubSdl); 119 } 120 121 override CheckStatus check() 122 { 123 auto res = appender!(string[]); 124 foreach (string file; sources) 125 { 126 auto content = readText(file); 127 auto pattern = "^ \\+ Copyright: %s$".format(copyright); 128 auto found = matchFirst(content, regex(pattern, "m")); 129 if (!found) 130 { 131 res.put(file); 132 } 133 } 134 noCopyrightFiles = res.data; 135 return (noCopyrightFiles.length == 0).to!CheckStatus; 136 } 137 138 override void run() 139 { 140 "Fixing copyright for %s".format(noCopyrightFiles).info; 141 142 foreach (file; noCopyrightFiles) 143 { 144 auto content = readText(file); 145 auto newContent = replaceFirst(content, regex("^ \\+ Copyright: .*?$", 146 "m"), " + Copyright: %s".format(copyright)); 147 if (content == newContent) 148 { 149 "Adding copyright %s to file %s".format(copyright, file).info; 150 newContent = "/++\n + Copyright: %s\n +/\n\n".format(copyright.to!string) ~ content; 151 } 152 else 153 { 154 "Change copyright to %s in file %s".format(copyright, file).info; 155 } 156 std.file.write(file, newContent); 157 } 158 } 159 } 160 161 class AuthorsPony : DlangPony 162 { 163 override string name() 164 { 165 return "Setup correct authors line in all .d files (taken from git log)"; 166 } 167 168 override CheckStatus check() 169 { 170 return CheckStatus.dont_know; 171 } 172 173 override void run() 174 { 175 foreach (file; sources) 176 { 177 import std.process; 178 179 auto content = readText(file); 180 auto authors = ["git", "log", "--pretty=format:%an", file].execute.output.split("\n") 181 .sort.uniq.join(", "); 182 auto authorsRegex = regex("^ \\+ Authors: (.*)$", "m"); 183 auto hasAuthorsLine = !content.matchFirst(authorsRegex).empty; 184 auto newContent = replaceFirst(content, authorsRegex, 185 " + Authors: %s".format(authors)); 186 if (hasAuthorsLine) 187 { 188 if (content == newContent) 189 { 190 "No change of authors in file %s".format(file).info; 191 } 192 else 193 { 194 "Change authors to %s in file %s".format(authors, file).info; 195 std.file.write(file, newContent); 196 } 197 } 198 else 199 { 200 "Adding authors line %s to file %s".format(authors, file).warning; 201 newContent = "/++\n + Authors: %s\n +/\n\n".format(authors) ~ content; 202 std.file.write(file, newContent); 203 } 204 } 205 206 } 207 } 208 209 class LicenseCommentPony : DlangPony 210 { 211 string[] noLicenseFiles; 212 string license; 213 214 this() 215 { 216 license = applicable ? getFromDubSdl("license") : null; 217 } 218 219 override string name() 220 { 221 return "Setup license headers in .d files (taken from %s)".format(dubSdl); 222 } 223 224 override CheckStatus check() 225 { 226 auto res = appender!(string[]); 227 foreach (string file; sources) 228 { 229 auto content = readText(file); 230 auto pattern = "^ \\+ License: %s$".format(license); 231 auto found = matchFirst(content, regex(pattern, "m")); 232 if (!found) 233 { 234 res.put(file); 235 } 236 } 237 noLicenseFiles = res.data; 238 return (noLicenseFiles.length == 0).to!CheckStatus; 239 } 240 241 override void run() 242 { 243 "Fixing license for %s".format(noLicenseFiles).info; 244 245 foreach (file; noLicenseFiles) 246 { 247 auto content = readText(file); 248 auto newContent = replaceFirst(content, regex("^ \\+ License: .*?$", 249 "m"), " + License: %s".format(license)); 250 if (content == newContent) 251 { 252 "Adding license %s to file %s".format(license, file).info; 253 newContent = "/++\n + License: %s\n +/\n\n".format(license.to!string) ~ content; 254 } 255 else 256 { 257 "Change license to %s in file %s".format(license, file).info; 258 } 259 std.file.write(file, newContent); 260 } 261 262 } 263 } 264 265 class GeneratePackageDependenciesPony : DlangPony 266 { 267 override string name() 268 { 269 return "Generate dependency diagrams."; 270 } 271 272 override CheckStatus check() 273 { 274 return CheckStatus.dont_know; 275 } 276 277 override void run() 278 { 279 import std.conv; 280 import std.file; 281 import std.json; 282 import std.stdio; 283 import std..string; 284 285 class Package 286 { 287 string name; 288 Package[] dependencies; 289 bool visited; 290 this(string name) 291 { 292 this.name = name; 293 } 294 295 auto addDependency(Package p) 296 { 297 dependencies ~= p; 298 } 299 300 override string toString() 301 { 302 return toString(""); 303 } 304 305 string toString(string indent) 306 { 307 string res = indent; 308 res ~= name ~ "\n"; 309 foreach (p; dependencies) 310 { 311 res ~= p.toString(indent ~ " "); 312 } 313 return res; 314 } 315 316 Package setVisited(bool value) 317 { 318 visited = value; 319 foreach (d; dependencies) 320 { 321 d.setVisited(value); 322 } 323 return this; 324 } 325 326 string toDot(string indent = "") 327 { 328 auto res = ""; 329 visited = true; 330 foreach (d; dependencies) 331 { 332 res ~= "\n%s->%s".format(name, d.name); 333 if (d.visited == false) 334 { 335 res ~= d.toDot(indent ~ " "); 336 } 337 } 338 return res; 339 } 340 341 } 342 343 struct Packages 344 { 345 Package[string] packages; 346 Package addOrGet(string name) 347 { 348 if (name !in packages) 349 { 350 auto newPackage = new Package(name); 351 packages[name] = newPackage; 352 } 353 return packages[name]; 354 } 355 } 356 357 import std.process; 358 359 "dub describe > out/dependencies.json".executeShell; 360 361 auto json = parseJSON(readText("out/dependencies.json")); 362 auto packages = Packages(); 363 auto rootPackage = json["rootPackage"].str; 364 writeln(rootPackage); 365 366 foreach (size_t index, value; json["packages"]) 367 { 368 auto packageName = value["name"]; 369 auto newPackage = packages.addOrGet(packageName.str); 370 foreach (size_t index, value; value["dependencies"]) 371 { 372 auto dep = packages.addOrGet(value.str); 373 newPackage.addDependency(dep); 374 } 375 } 376 377 stderr.writeln(packages.addOrGet(rootPackage).to!string); 378 auto dot = "digraph G {%s\n}\n".format(packages.addOrGet(rootPackage) 379 .setVisited(false).toDot); 380 std.file.write("out/dependencies.dot", dot); 381 382 import std.process; 383 384 ["dot", "out/dependencies.dot", "-Tpng", "-o", "docs/images/dependencies.png"].execute; 385 ["dot", "out/dependencies.dot", "-Tsvg", "-o", "docs/images/dependencies.svg"].execute; 386 387 } 388 389 } 390 391 class AddPackageVersionPony : DlangPony 392 { 393 string packageName; 394 string preBuildCommands; 395 string sourceFiles; 396 auto sourcePaths = "sourcePaths \"source\" \"out/generated/packageversion\"\n"; 397 auto importPaths = "importPaths \"source\" \"out/generated/packageversion\"\n"; 398 auto dubFetchPackageVersion = "dub fetch packageversion"; 399 auto addPackageVersionDependency = `dependency "packageversion" version="~>0.0.12" 400 subConfiguration "packageversion" "library" 401 `; 402 this() 403 { 404 packageName = getFromDubSdl("name"); 405 preBuildCommands = applicable ? "preBuildCommands \"dub run packageversion -- --packageName=%s\"\n".format( 406 packageName) : null; 407 sourceFiles = applicable ? "sourceFile \"out/generated/packageversion/%s/packageversion.d".format(packageName) 408 : null; 409 } 410 411 override string name() 412 { 413 return "Add automatic generation of package version to %s".format(dubSdl); 414 } 415 416 override CheckStatus check() 417 { 418 auto dubSdlContent = readText(dubSdl); 419 auto travisYml = readText(travisYml); 420 return (dubSdlContent.canFind(sourcePaths) && dubSdlContent.canFind(importPaths) 421 && dubSdlContent.canFind(preBuildCommands) && dubSdlContent.canFind(sourceFiles) 422 && dubSdlContent.canFind(addPackageVersionDependency) 423 && travisYml.canFind(dubFetchPackageVersion)).to!CheckStatus; 424 } 425 426 override void run() 427 { 428 auto oldContent = readText(dubSdl); 429 auto content = oldContent; 430 if (!content.canFind(sourcePaths)) 431 { 432 "Adding sourcePaths to %s".format(dubSdl).info; 433 content ~= sourcePaths; 434 } 435 436 if (!content.canFind(importPaths)) 437 { 438 "Adding importPaths to %s".format(dubSdl).info; 439 content ~= importPaths; 440 } 441 if (!content.canFind(preBuildCommands)) 442 { 443 "Adding preBuildCommands to %s".format(dubSdl).info; 444 content ~= preBuildCommands; 445 } 446 if (!content.canFind(sourceFiles)) 447 { 448 "Adding sourceFiles to %s".format(dubSdl).info; 449 content ~= sourceFiles; 450 } 451 452 if (!content.canFind(addPackageVersionDependency)) 453 { 454 "Adding packageversion dependency to %s".format(dubSdl).info; 455 content ~= addPackageVersionDependency; 456 } 457 if (content != oldContent) 458 { 459 "Writing new %s".format(dubSdl).info; 460 std.file.write(dubSdl, content); 461 } 462 463 auto root = Loader(travisYml).load; 464 auto beforeInstall = root["before_install"]; 465 if (beforeInstall.isScalar) 466 { 467 if (beforeInstall.as!string != dubFetchPackageVersion) 468 { 469 "adding %s to %s".format(dubFetchPackageVersion, travisYml).info; 470 root["before_install"] = Node([beforeInstall, Node(dubFetchPackageVersion)]); 471 Dumper(travisYml).dump(root); 472 } 473 } 474 else if (beforeInstall.isSequence) 475 { 476 if (!beforeInstall.sequence!string.canFind(dubFetchPackageVersion)) 477 { 478 "adding %s to %s".format(dubFetchPackageVersion, travisYml).info; 479 beforeInstall.add(Node(dubFetchPackageVersion)); 480 root["before_install"] = beforeInstall; 481 Dumper(travisYml).dump(root); 482 } 483 } 484 else 485 { 486 throw new Exception("cannot process %s".format(travisYml)); 487 } 488 } 489 }