1 /** 2 Stuff with dependencies. 3 4 Copyright: © 2012-2013 Matthias Dondorff, © 2012-2015 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff 7 */ 8 module dub.package_; 9 10 public import dub.recipe.packagerecipe; 11 12 import dub.compilers.compiler; 13 import dub.dependency; 14 import dub.description; 15 import dub.recipe.json; 16 import dub.recipe.sdl; 17 18 import dub.internal.utils; 19 import dub.internal.vibecompat.core.log; 20 import dub.internal.vibecompat.core.file; 21 import dub.internal.vibecompat.data.json; 22 import dub.internal.vibecompat.inet.url; 23 24 import std.algorithm; 25 import std.array; 26 import std.conv; 27 import std.exception; 28 import std.file; 29 import std.range; 30 import std..string; 31 import std.typecons : Nullable; 32 33 34 35 enum PackageFormat { json, sdl } 36 struct FilenameAndFormat 37 { 38 string filename; 39 PackageFormat format; 40 } 41 struct PathAndFormat 42 { 43 Path path; 44 PackageFormat format; 45 @property bool empty() { return path.empty; } 46 string toString() { return path.toString(); } 47 } 48 49 // Supported package descriptions in decreasing order of preference. 50 static immutable FilenameAndFormat[] packageInfoFiles = [ 51 {"dub.json", PackageFormat.json}, 52 /*{"dub.sdl",PackageFormat.sdl},*/ 53 {"package.json", PackageFormat.json} 54 ]; 55 56 @property string[] packageInfoFilenames() { return packageInfoFiles.map!(f => cast(string)f.filename).array; } 57 58 @property string defaultPackageFilename() { return packageInfoFiles[0].filename; } 59 60 61 /** 62 Represents a package, including its sub packages 63 64 Documentation of the dub.json can be found at 65 http://registry.vibed.org/package-format 66 */ 67 class Package { 68 private { 69 Path m_path; 70 PathAndFormat m_infoFile; 71 PackageRecipe m_info; 72 Package m_parentPackage; 73 } 74 75 static PathAndFormat findPackageFile(Path path) 76 { 77 foreach(file; packageInfoFiles) { 78 auto filename = path ~ file.filename; 79 if(existsFile(filename)) return PathAndFormat(filename, file.format); 80 } 81 return PathAndFormat(Path()); 82 } 83 84 this(Path root, PathAndFormat infoFile = PathAndFormat(), Package parent = null, string versionOverride = "") 85 { 86 RawPackage raw_package; 87 m_infoFile = infoFile; 88 89 try { 90 if(m_infoFile.empty) { 91 m_infoFile = findPackageFile(root); 92 if(m_infoFile.empty) 93 throw new Exception( 94 "No package file found in %s, expected one of %s" 95 .format(root.toNativeString(), packageInfoFiles.map!(f => cast(string)f.filename).join("/"))); 96 } 97 raw_package = rawPackageFromFile(m_infoFile); 98 } catch (Exception ex) throw ex;//throw new Exception(format("Failed to load package %s: %s", m_infoFile.toNativeString(), ex.msg)); 99 100 enforce(raw_package !is null, format("Missing package description for package at %s", root.toNativeString())); 101 this(raw_package, root, parent, versionOverride); 102 } 103 104 this(Json package_info, Path root = Path(), Package parent = null, string versionOverride = "") 105 { 106 this(new JsonPackage(package_info), root, parent, versionOverride); 107 } 108 109 this(RawPackage raw_package, Path root = Path(), Package parent = null, string versionOverride = "") 110 { 111 PackageRecipe recipe; 112 113 // parse the Package description 114 if(raw_package !is null) 115 { 116 scope(failure) logError("Failed to parse package description for %s %s in %s.", 117 raw_package.package_name, versionOverride.length ? versionOverride : raw_package.version_, 118 root.length ? root.toNativeString() : "remote location"); 119 raw_package.parseInto(recipe, parent ? parent.name : null); 120 121 if (!versionOverride.empty) 122 recipe.version_ = versionOverride; 123 124 // try to run git to determine the version of the package if no explicit version was given 125 if (recipe.version_.length == 0 && !parent) { 126 try recipe.version_ = determineVersionFromSCM(root); 127 catch (Exception e) logDebug("Failed to determine version by SCM: %s", e.msg); 128 129 if (recipe.version_.length == 0) { 130 logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", recipe.name, this.path.toNativeString()); 131 // TODO: Assume unknown version here? 132 // recipe.version_ = Version.UNKNOWN.toString(); 133 recipe.version_ = Version.MASTER.toString(); 134 } else logDiagnostic("Determined package version using GIT: %s %s", recipe.name, recipe.version_); 135 } 136 } 137 138 this(recipe, root, parent); 139 } 140 141 this(PackageRecipe recipe, Path root = Path(), Package parent = null) 142 { 143 m_parentPackage = parent; 144 m_path = root; 145 m_path.endsWithSlash = true; 146 147 // use the given recipe as the basis 148 m_info = recipe; 149 150 fillWithDefaults(); 151 simpleLint(); 152 } 153 154 @property string name() 155 const { 156 if (m_parentPackage) return m_parentPackage.name ~ ":" ~ m_info.name; 157 else return m_info.name; 158 } 159 @property string vers() const { return m_parentPackage ? m_parentPackage.vers : m_info.version_; } 160 @property Version ver() const { return Version(this.vers); } 161 @property void ver(Version ver) { assert(m_parentPackage is null); m_info.version_ = ver.toString(); } 162 @property ref inout(PackageRecipe) info() inout { return m_info; } 163 @property Path path() const { return m_path; } 164 @property Path packageInfoFilename() const { return m_infoFile.path; } 165 @property const(Dependency[string]) dependencies() const { return m_info.dependencies; } 166 @property inout(Package) basePackage() inout { return m_parentPackage ? m_parentPackage.basePackage : this; } 167 @property inout(Package) parentPackage() inout { return m_parentPackage; } 168 @property inout(SubPackage)[] subPackages() inout { return m_info.subPackages; } 169 170 @property string[] configurations() 171 const { 172 auto ret = appender!(string[])(); 173 foreach( ref config; m_info.configurations ) 174 ret.put(config.name); 175 return ret.data; 176 } 177 178 const(Dependency[string]) getDependencies(string config) 179 const { 180 Dependency[string] ret; 181 foreach (k, v; m_info.buildSettings.dependencies) 182 ret[k] = v; 183 foreach (ref conf; m_info.configurations) 184 if (conf.name == config) { 185 foreach (k, v; conf.buildSettings.dependencies) 186 ret[k] = v; 187 break; 188 } 189 return ret; 190 } 191 192 /** Overwrites the packge description file using the default filename with the current information. 193 */ 194 void storeInfo() 195 { 196 storeInfo(m_path); 197 m_infoFile = PathAndFormat(m_path ~ defaultPackageFilename); 198 } 199 /// ditto 200 void storeInfo(Path path) 201 const { 202 enforce(!ver.isUnknown, "Trying to store a package with an 'unknown' version, this is not supported."); 203 auto filename = path ~ defaultPackageFilename; 204 auto dstFile = openFile(filename.toNativeString(), FileMode.CreateTrunc); 205 scope(exit) dstFile.close(); 206 dstFile.writePrettyJsonString(m_info.toJson()); 207 } 208 209 Nullable!PackageRecipe getInternalSubPackage(string name) 210 { 211 foreach (ref p; m_info.subPackages) 212 if (p.path.empty && p.recipe.name == name) 213 return Nullable!PackageRecipe(p.recipe); 214 return Nullable!PackageRecipe(); 215 } 216 217 void warnOnSpecialCompilerFlags() 218 { 219 // warn about use of special flags 220 m_info.buildSettings.warnOnSpecialCompilerFlags(m_info.name, null); 221 foreach (ref config; m_info.configurations) 222 config.buildSettings.warnOnSpecialCompilerFlags(m_info.name, config.name); 223 } 224 225 const(BuildSettingsTemplate) getBuildSettings(string config = null) 226 const { 227 if (config.length) { 228 foreach (ref conf; m_info.configurations) 229 if (conf.name == config) 230 return conf.buildSettings; 231 assert(false, "Unknown configuration: "~config); 232 } else { 233 return m_info.buildSettings; 234 } 235 } 236 237 /// Returns all BuildSettings for the given platform and config. 238 BuildSettings getBuildSettings(in BuildPlatform platform, string config) 239 const { 240 BuildSettings ret; 241 m_info.buildSettings.getPlatformSettings(ret, platform, this.path); 242 bool found = false; 243 foreach(ref conf; m_info.configurations){ 244 if( conf.name != config ) continue; 245 conf.buildSettings.getPlatformSettings(ret, platform, this.path); 246 found = true; 247 break; 248 } 249 assert(found || config is null, "Unknown configuration for "~m_info.name~": "~config); 250 251 // construct default target name based on package name 252 if( ret.targetName.empty ) ret.targetName = this.name.replace(":", "_"); 253 254 // special support for DMD style flags 255 getCompiler("dmd").extractBuildOptions(ret); 256 257 return ret; 258 } 259 260 /// Returns the combination of all build settings for all configurations and platforms 261 BuildSettings getCombinedBuildSettings() 262 const { 263 BuildSettings ret; 264 m_info.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path); 265 foreach(ref conf; m_info.configurations) 266 conf.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path); 267 268 // construct default target name based on package name 269 if (ret.targetName.empty) ret.targetName = this.name.replace(":", "_"); 270 271 // special support for DMD style flags 272 getCompiler("dmd").extractBuildOptions(ret); 273 274 return ret; 275 } 276 277 void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type) 278 const { 279 if (build_type == "$DFLAGS") { 280 import std.process; 281 string dflags = environment.get("DFLAGS"); 282 settings.addDFlags(dflags.split()); 283 return; 284 } 285 286 if (auto pbt = build_type in m_info.buildTypes) { 287 logDiagnostic("Using custom build type '%s'.", build_type); 288 pbt.getPlatformSettings(settings, platform, this.path); 289 } else { 290 with(BuildOption) switch (build_type) { 291 default: throw new Exception(format("Unknown build type for %s: '%s'", this.name, build_type)); 292 case "plain": break; 293 case "debug": settings.addOptions(debugMode, debugInfo); break; 294 case "release": settings.addOptions(releaseMode, optimize, inline); break; 295 case "release-nobounds": settings.addOptions(releaseMode, optimize, inline, noBoundsCheck); break; 296 case "unittest": settings.addOptions(unittests, debugMode, debugInfo); break; 297 case "docs": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Dddocs"); break; 298 case "ddox": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Df__dummy.html", "-Xfdocs.json"); break; 299 case "profile": settings.addOptions(profile, optimize, inline, debugInfo); break; 300 case "cov": settings.addOptions(coverage, debugInfo); break; 301 case "unittest-cov": settings.addOptions(unittests, coverage, debugMode, debugInfo); break; 302 } 303 } 304 } 305 306 string getSubConfiguration(string config, in Package dependency, in BuildPlatform platform) 307 const { 308 bool found = false; 309 foreach(ref c; m_info.configurations){ 310 if( c.name == config ){ 311 if( auto pv = dependency.name in c.buildSettings.subConfigurations ) return *pv; 312 found = true; 313 break; 314 } 315 } 316 assert(found || config is null, "Invalid configuration \""~config~"\" for "~this.name); 317 if( auto pv = dependency.name in m_info.buildSettings.subConfigurations ) return *pv; 318 return null; 319 } 320 321 /// Returns the default configuration to build for the given platform 322 string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library = false) 323 const { 324 foreach (ref conf; m_info.configurations) { 325 if (!conf.matchesPlatform(platform)) continue; 326 if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue; 327 return conf.name; 328 } 329 return null; 330 } 331 332 /// Returns a list of configurations suitable for the given platform 333 string[] getPlatformConfigurations(in BuildPlatform platform, bool is_main_package = false) 334 const { 335 auto ret = appender!(string[]); 336 foreach(ref conf; m_info.configurations){ 337 if (!conf.matchesPlatform(platform)) continue; 338 if (!is_main_package && conf.buildSettings.targetType == TargetType.executable) continue; 339 ret ~= conf.name; 340 } 341 if (ret.data.length == 0) ret.put(null); 342 return ret.data; 343 } 344 345 /// Human readable information of this package and its dependencies. 346 string generateInfoString() const { 347 string s; 348 s ~= m_info.name ~ ", version '" ~ m_info.version_ ~ "'"; 349 s ~= "\n Dependencies:"; 350 foreach(string p, ref const Dependency v; m_info.dependencies) 351 s ~= "\n " ~ p ~ ", version '" ~ v.toString() ~ "'"; 352 return s; 353 } 354 355 bool hasDependency(string depname, string config) 356 const { 357 if (depname in m_info.buildSettings.dependencies) return true; 358 foreach (ref c; m_info.configurations) 359 if ((config.empty || c.name == config) && depname in c.buildSettings.dependencies) 360 return true; 361 return false; 362 } 363 364 /** Returns a description of the package for use in IDEs or build tools. 365 */ 366 PackageDescription describe(BuildPlatform platform, string config) 367 const { 368 PackageDescription ret; 369 ret.path = m_path.toNativeString(); 370 ret.name = this.name; 371 ret.version_ = this.ver; 372 ret.description = m_info.description; 373 ret.homepage = m_info.homepage; 374 ret.authors = m_info.authors.dup; 375 ret.copyright = m_info.copyright; 376 ret.license = m_info.license; 377 ret.dependencies = getDependencies(config).keys; 378 379 // save build settings 380 BuildSettings bs = getBuildSettings(platform, config); 381 BuildSettings allbs = getCombinedBuildSettings(); 382 383 ret.targetType = bs.targetType; 384 ret.targetPath = bs.targetPath; 385 ret.targetName = bs.targetName; 386 if (ret.targetType != TargetType.none) 387 ret.targetFileName = getTargetFileName(bs, platform); 388 ret.workingDirectory = bs.workingDirectory; 389 ret.mainSourceFile = bs.mainSourceFile; 390 ret.dflags = bs.dflags; 391 ret.lflags = bs.lflags; 392 ret.libs = bs.libs; 393 ret.copyFiles = bs.copyFiles; 394 ret.versions = bs.versions; 395 ret.debugVersions = bs.debugVersions; 396 ret.importPaths = bs.importPaths; 397 ret.stringImportPaths = bs.stringImportPaths; 398 ret.preGenerateCommands = bs.preGenerateCommands; 399 ret.postGenerateCommands = bs.postGenerateCommands; 400 ret.preBuildCommands = bs.preBuildCommands; 401 ret.postBuildCommands = bs.postBuildCommands; 402 403 // prettify build requirements output 404 for (int i = 1; i <= BuildRequirement.max; i <<= 1) 405 if (bs.requirements & cast(BuildRequirement)i) 406 ret.buildRequirements ~= cast(BuildRequirement)i; 407 408 // prettify options output 409 for (int i = 1; i <= BuildOption.max; i <<= 1) 410 if (bs.options & cast(BuildOption)i) 411 ret.options ~= cast(BuildOption)i; 412 413 // collect all possible source files and determine their types 414 SourceFileRole[string] sourceFileTypes; 415 foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.unusedStringImport; 416 foreach (f; allbs.importFiles) sourceFileTypes[f] = SourceFileRole.unusedImport; 417 foreach (f; allbs.sourceFiles) sourceFileTypes[f] = SourceFileRole.unusedSource; 418 foreach (f; bs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.stringImport; 419 foreach (f; bs.importFiles) sourceFileTypes[f] = SourceFileRole.import_; 420 foreach (f; bs.sourceFiles) sourceFileTypes[f] = SourceFileRole.source; 421 foreach (f; sourceFileTypes.byKey.array.sort()) { 422 SourceFileDescription sf; 423 sf.path = f; 424 sf.type = sourceFileTypes[f]; 425 ret.files ~= sf; 426 } 427 428 return ret; 429 } 430 // ditto 431 deprecated void describe(ref Json dst, BuildPlatform platform, string config) 432 { 433 auto res = describe(platform, config); 434 foreach (string key, value; res.serializeToJson()) 435 dst[key] = value; 436 } 437 438 private void fillWithDefaults() 439 { 440 auto bs = &m_info.buildSettings; 441 442 // check for default string import folders 443 if ("" !in bs.stringImportPaths) { 444 foreach(defvf; ["views"]){ 445 if( existsFile(m_path ~ defvf) ) 446 bs.stringImportPaths[""] ~= defvf; 447 } 448 } 449 450 // check for default source folders 451 immutable hasSP = ("" in bs.sourcePaths) !is null; 452 immutable hasIP = ("" in bs.importPaths) !is null; 453 if (!hasSP || !hasIP) { 454 foreach (defsf; ["source/", "src/"]) { 455 if (existsFile(m_path ~ defsf)) { 456 if (!hasSP) bs.sourcePaths[""] ~= defsf; 457 if (!hasIP) bs.importPaths[""] ~= defsf; 458 } 459 } 460 } 461 462 // check for default app_main 463 string app_main_file; 464 auto pkg_name = m_info.name.length ? m_info.name : "unknown"; 465 foreach(sf; bs.sourcePaths.get("", null)){ 466 auto p = m_path ~ sf; 467 if( !existsFile(p) ) continue; 468 foreach(fil; ["app.d", "main.d", pkg_name ~ "/main.d", pkg_name ~ "/" ~ "app.d"]){ 469 if( existsFile(p ~ fil) ) { 470 app_main_file = (Path(sf) ~ fil).toNativeString(); 471 break; 472 } 473 } 474 } 475 476 // generate default configurations if none are defined 477 if (m_info.configurations.length == 0) { 478 if (bs.targetType == TargetType.executable) { 479 BuildSettingsTemplate app_settings; 480 app_settings.targetType = TargetType.executable; 481 if (bs.mainSourceFile.empty) app_settings.mainSourceFile = app_main_file; 482 m_info.configurations ~= ConfigurationInfo("application", app_settings); 483 } else if (bs.targetType != TargetType.none) { 484 BuildSettingsTemplate lib_settings; 485 lib_settings.targetType = bs.targetType == TargetType.autodetect ? TargetType.library : bs.targetType; 486 487 if (bs.targetType == TargetType.autodetect) { 488 if (app_main_file.length) { 489 lib_settings.excludedSourceFiles[""] ~= app_main_file; 490 491 BuildSettingsTemplate app_settings; 492 app_settings.targetType = TargetType.executable; 493 app_settings.mainSourceFile = app_main_file; 494 m_info.configurations ~= ConfigurationInfo("application", app_settings); 495 } 496 } 497 498 m_info.configurations ~= ConfigurationInfo("library", lib_settings); 499 } 500 } 501 } 502 503 private void simpleLint() const { 504 if (m_parentPackage) { 505 if (m_parentPackage.path != path) { 506 if (info.license.length && info.license != m_parentPackage.info.license) 507 logWarn("License in subpackage %s is different than it's parent package, this is discouraged.", name); 508 } 509 } 510 if (name.empty) logWarn("The package in %s has no name.", path); 511 } 512 513 private static RawPackage rawPackageFromFile(PathAndFormat file, bool silent_fail = false) { 514 if( silent_fail && !existsFile(file.path) ) return null; 515 516 string text; 517 518 { 519 auto f = openFile(file.path.toNativeString(), FileMode.Read); 520 scope(exit) f.close(); 521 text = stripUTF8Bom(cast(string)f.readAll()); 522 } 523 524 final switch(file.format) { 525 case PackageFormat.json: 526 return new JsonPackage(parseJsonString(text, file.path.toNativeString())); 527 case PackageFormat.sdl: 528 if(silent_fail) return null; throw new Exception("SDL not implemented"); 529 } 530 } 531 532 static abstract class RawPackage 533 { 534 string package_name; // Should already be lower case 535 string version_; 536 abstract void parseInto(ref PackageRecipe package_, string parent_name); 537 } 538 private static class JsonPackage : RawPackage 539 { 540 Json json; 541 this(Json json) { 542 this.json = json; 543 544 string nameLower; 545 if(json.type == Json.Type..string) { 546 nameLower = json.get!string.toLower(); 547 this.json = nameLower; 548 } else { 549 nameLower = json.name.get!string.toLower(); 550 this.json.name = nameLower; 551 this.package_name = nameLower; 552 553 Json versionJson = json["version"]; 554 this.version_ = (versionJson.type == Json.Type.undefined) ? null : versionJson.get!string; 555 } 556 557 this.package_name = nameLower; 558 } 559 override void parseInto(ref PackageRecipe recipe, string parent_name) 560 { 561 recipe.parseJson(json, parent_name); 562 } 563 } 564 private static class SdlPackage : RawPackage 565 { 566 override void parseInto(ref PackageRecipe package_, string parent_name) 567 { 568 throw new Exception("SDL packages not implemented yet"); 569 } 570 } 571 } 572 573 private string determineVersionFromSCM(Path path) 574 { 575 import std.process; 576 import dub.semver; 577 578 auto git_dir = path ~ ".git"; 579 if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null; 580 auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString(); 581 582 static string exec(scope string[] params...) { 583 auto ret = executeShell(escapeShellCommand(params)); 584 if (ret.status == 0) return ret.output.strip; 585 logDebug("'%s' failed with exit code %s: %s", params.join(" "), ret.status, ret.output.strip); 586 return null; 587 } 588 589 auto tag = exec("git", git_dir_param, "describe", "--long", "--tags"); 590 if (tag !is null) { 591 auto parts = tag.split("-"); 592 auto commit = parts[$-1]; 593 auto num = parts[$-2].to!int; 594 tag = parts[0 .. $-2].join("-"); 595 if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) { 596 if (num == 0) return tag[1 .. $]; 597 else if (tag.canFind("+")) return format("%s.commit.%s.%s", tag[1 .. $], num, commit); 598 else return format("%s+commit.%s.%s", tag[1 .. $], num, commit); 599 } 600 } 601 602 auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD"); 603 if (branch !is null) { 604 if (branch != "HEAD") return "~" ~ branch; 605 } 606 607 return null; 608 }