1 /** 2 Representing a full project, with a root Package and several dependencies. 3 4 Copyright: © 2012-2013 Matthias Dondorff 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff, Sönke Ludwig 7 */ 8 module dub.project; 9 10 import dub.compilers.compiler; 11 import dub.dependency; 12 import dub.internal.utils; 13 import dub.internal.vibecompat.core.file; 14 import dub.internal.vibecompat.core.log; 15 import dub.internal.vibecompat.data.json; 16 import dub.internal.vibecompat.inet.url; 17 import dub.package_; 18 import dub.packagemanager; 19 import dub.packagesupplier; 20 import dub.generators.generator; 21 22 23 // todo: cleanup imports. 24 import std.algorithm; 25 import std.array; 26 import std.conv; 27 import std.datetime; 28 import std.exception; 29 import std.file; 30 import std.process; 31 import std.string; 32 import std.typecons; 33 import std.zip; 34 35 36 /// Representing a full project, with a root Package and several dependencies. 37 class Project { 38 private { 39 bool m_fixedPackage; 40 Path m_root; 41 PackageManager m_packageManager; 42 Json m_json; 43 Package m_main; 44 //Package[string] m_packages; 45 Package[] m_dependencies; 46 Package[][Package] m_dependees; 47 } 48 49 this(PackageManager package_manager, Path project_path) 50 { 51 m_packageManager = package_manager; 52 m_root = project_path; 53 m_fixedPackage = false; 54 m_json = Json.emptyObject; 55 reinit(); 56 } 57 58 this(PackageManager package_manager, Package pack) 59 { 60 m_packageManager = package_manager; 61 m_root = pack.path; 62 m_main = pack; 63 m_fixedPackage = true; 64 m_json = Json.emptyObject; 65 reinit(); 66 } 67 68 /// Gathers information 69 @property string info() 70 const { 71 if(!m_main) 72 return "-Unrecognized application in '"~m_root.toNativeString()~"' (probably no package.json in this directory)"; 73 string s = "-Application identifier: " ~ m_main.name; 74 s ~= "\n" ~ m_main.generateInfoString(); 75 s ~= "\n-Retrieved dependencies:"; 76 foreach(p; m_dependencies) 77 s ~= "\n" ~ p.generateInfoString(); 78 return s; 79 } 80 81 /// Gets all retrieved packages as a "packageId" = "version" associative array 82 @property string[string] cachedPackagesIDs() const { 83 string[string] pkgs; 84 foreach(p; m_dependencies) 85 pkgs[p.name] = p.vers; 86 return pkgs; 87 } 88 89 /// List of retrieved dependency Packages 90 @property const(Package[]) dependencies() const { return m_dependencies; } 91 92 /// Main package. 93 @property inout(Package) mainPackage() inout { return m_main; } 94 95 /** Allows iteration of the dependency tree in topological order 96 */ 97 int delegate(int delegate(ref const Package)) getTopologicalPackageList(bool children_first = false, in Package root_package = null, string[string] configs = null) 98 const { 99 const(Package) rootpack = root_package ? root_package : m_main; 100 101 int iterator(int delegate(ref const Package) del) 102 { 103 int ret = 0; 104 bool[const(Package)] visited; 105 void perform_rec(in Package p){ 106 if( p in visited ) return; 107 visited[p] = true; 108 109 if( !children_first ){ 110 ret = del(p); 111 if( ret ) return; 112 } 113 114 auto cfg = configs.get(p.name, null); 115 116 foreach (dn, dv; p.dependencies) { 117 // filter out dependencies not in the current configuration set 118 if (!p.hasDependency(dn, cfg)) continue; 119 auto dependency = getDependency(dn, dv.optional); 120 if(dependency) perform_rec(dependency); 121 if( ret ) return; 122 } 123 124 if( children_first ){ 125 ret = del(p); 126 if( ret ) return; 127 } 128 } 129 perform_rec(rootpack); 130 return ret; 131 } 132 133 return &iterator; 134 } 135 136 inout(Package) getDependency(string name, bool isOptional) 137 inout { 138 foreach(dp; m_dependencies) 139 if( dp.name == name ) 140 return dp; 141 if(!isOptional) throw new Exception("Unknown dependency: "~name); 142 else return null; 143 } 144 145 string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) 146 const { 147 auto cfgs = getPackageConfigs(platform, null, allow_non_library_configs); 148 return cfgs[m_main.name]; 149 } 150 151 /// Rereads the applications state. 152 void reinit() 153 { 154 scope(failure){ 155 logDiagnostic("Failed to initialize project. Assuming defaults."); 156 if (!m_fixedPackage) m_main = new Package(serializeToJson(["name": "unknown"]), m_root); 157 } 158 159 m_dependencies = null; 160 m_packageManager.refresh(false); 161 162 try m_json = jsonFromFile(m_root ~ ".dub/dub.json", true); 163 catch(Exception t) logDiagnostic("Failed to read .dub/dub.json: %s", t.msg); 164 165 // load package description 166 if (!m_fixedPackage) { 167 if (!Package.isPackageAt(m_root)) { 168 logWarn("There was no package description found for the application in '%s'.", m_root.toNativeString()); 169 auto json = Json.emptyObject; 170 json.name = "unknown"; 171 m_main = new Package(json, m_root); 172 return; 173 } 174 175 m_main = m_packageManager.getPackage(m_root); 176 } 177 178 // some basic package lint 179 m_main.warnOnSpecialCompilerFlags(); 180 if (m_main.name != m_main.name.toLower()) { 181 logWarn(`DUB package names should always be lower case, please change to {"name": "%s"}. You can use {"targetName": "%s"} to keep the current executable name.`, 182 m_main.name.toLower(), m_main.name); 183 } 184 185 // TODO: compute the set of mutual dependencies first 186 // (i.e. ">=0.0.1 <=0.0.5" and "<= 0.0.4" get ">=0.0.1 <=0.0.4") 187 // conflicts would then also be detected. 188 void collectDependenciesRec(Package pack) 189 { 190 logDiagnostic("Collecting dependencies for %s", pack.name); 191 foreach( name, vspec; pack.dependencies ){ 192 Package p; 193 if( !vspec.path.empty ){ 194 Path path = vspec.path; 195 if( !path.absolute ) path = pack.path ~ path; 196 logDiagnostic("Adding local %s %s", path, vspec.version_); 197 p = m_packageManager.getTemporaryPackage(path, vspec.version_); 198 } else { 199 p = m_packageManager.getBestPackage(name, vspec); 200 } 201 if( !m_dependencies.canFind(p) ){ 202 logDiagnostic("Found dependency %s %s: %s", name, vspec.toString(), p !is null); 203 if( p ){ 204 m_dependencies ~= p; 205 p.warnOnSpecialCompilerFlags(); 206 collectDependenciesRec(p); 207 } 208 } 209 m_dependees[p] ~= pack; 210 //enforce(p !is null, "Failed to resolve dependency "~name~" "~vspec.toString()); 211 } 212 } 213 collectDependenciesRec(m_main); 214 } 215 216 /// Returns the applications name. 217 @property string name() const { return m_main ? m_main.name : "app"; } 218 219 @property string[] configurations() const { return m_main.configurations; } 220 221 /// Returns a map with the configuration for all packages in the dependency tree. 222 string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true) 223 const { 224 struct Vertex { string pack, config; } 225 struct Edge { size_t from, to; } 226 227 Vertex[] configs; 228 Edge[] edges; 229 string[][string] parents; 230 parents[m_main.name] = null; 231 foreach (p; getTopologicalPackageList()) 232 foreach (d; p.dependencies.byKey) 233 parents[d] ~= p.name; 234 235 236 size_t createConfig(string pack, string config) { 237 foreach (i, v; configs) 238 if (v.pack == pack && v.config == config) 239 return i; 240 logDebug("Add config %s %s", pack, config); 241 configs ~= Vertex(pack, config); 242 return configs.length-1; 243 } 244 245 bool haveConfig(string pack, string config) { 246 return configs.any!(c => c.pack == pack && c.config == config); 247 } 248 249 size_t createEdge(size_t from, size_t to) { 250 auto idx = edges.countUntil(Edge(from, to)); 251 if (idx >= 0) return idx; 252 logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config); 253 edges ~= Edge(from, to); 254 return edges.length-1; 255 } 256 257 void removeConfig(size_t i) { 258 logDebug("Eliminating config %s for %s", configs[i].config, configs[i].pack); 259 configs = configs.remove(i); 260 edges = edges.filter!(e => e.from != i && e.to != i).array(); 261 foreach (ref e; edges) { 262 if (e.from > i) e.from--; 263 if (e.to > i) e.to--; 264 } 265 } 266 267 bool isReachable(string pack, string conf) { 268 if (pack == configs[0].pack && configs[0].config == conf) return true; 269 foreach (e; edges) 270 if (configs[e.to].pack == pack && configs[e.to].config == conf) 271 return true; 272 return false; 273 //return (pack == configs[0].pack && conf == configs[0].config) || edges.canFind!(e => configs[e.to].pack == pack && configs[e.to].config == config); 274 } 275 276 bool isReachableByAllParentPacks(size_t cidx) { 277 bool[string] r; 278 foreach (p; parents[configs[cidx].pack]) r[p] = false; 279 foreach (e; edges) { 280 if (e.to != cidx) continue; 281 if (auto pp = configs[e.from].pack in r) *pp = true; 282 } 283 foreach (bool v; r) if (!v) return false; 284 return true; 285 } 286 287 // create a graph of all possible package configurations (package, config) -> (subpackage, subconfig) 288 void determineAllConfigs(in Package p) 289 { 290 // first, add all dependency configurations 291 foreach (dn; p.dependencies.byKey) { 292 auto dp = getDependency(dn, true); 293 if (!dp) continue; 294 determineAllConfigs(dp); 295 } 296 297 // for each configuration, determine the configurations usable for the dependencies 298 outer: foreach (c; p.getPlatformConfigurations(platform, p is m_main && allow_non_library)) { 299 string[][string] depconfigs; 300 foreach (dn; p.dependencies.byKey) { 301 auto dp = getDependency(dn, true); 302 if (!dp) continue; 303 304 string[] cfgs; 305 auto subconf = p.getSubConfiguration(c, dp, platform); 306 if (!subconf.empty) cfgs = [subconf]; 307 else cfgs = dp.getPlatformConfigurations(platform); 308 cfgs = cfgs.filter!(c => haveConfig(dn, c)).array; 309 310 // if no valid configuration was found for a dependency, don't include the 311 // current configuration 312 if (!cfgs.length) { 313 logDebug("Skip %s %s (missing configuration for %s)", p.name, c, dp.name); 314 continue outer; 315 } 316 depconfigs[dn] = cfgs; 317 } 318 319 // add this configuration to the graph 320 size_t cidx = createConfig(p.name, c); 321 foreach (dn; p.dependencies.byKey) 322 foreach (sc; depconfigs.get(dn, null)) 323 createEdge(cidx, createConfig(dn, sc)); 324 } 325 } 326 if (config.length) createConfig(m_main.name, config); 327 determineAllConfigs(m_main); 328 329 // successively remove configurations until only one configuration per package is left 330 bool changed; 331 do { 332 // remove all configs that are not reachable by all parent packages 333 changed = false; 334 for (size_t i = 0; i < configs.length; ) { 335 if (!isReachableByAllParentPacks(i)) { 336 logDebug("NOT REACHABLE by (%s):", parents[configs[i].pack]); 337 removeConfig(i); 338 changed = true; 339 } else i++; 340 } 341 342 // when all edges are cleaned up, pick one package and remove all but one config 343 if (!changed) { 344 foreach (p; getTopologicalPackageList()) { 345 size_t cnt = 0; 346 for (size_t i = 0; i < configs.length; ) { 347 if (configs[i].pack == p.name) { 348 if (++cnt > 1) { 349 logDebug("NON-PRIMARY:"); 350 removeConfig(i); 351 } else i++; 352 } else i++; 353 } 354 if (cnt > 1) { 355 changed = true; 356 break; 357 } 358 } 359 } 360 } while (changed); 361 362 // print out the resulting tree 363 foreach (e; edges) logDebug(" %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config); 364 365 // return the resulting configuration set as an AA 366 string[string] ret; 367 foreach (c; configs) { 368 assert(ret.get(c.pack, c.config) == c.config, format("Conflicting configurations for %s found: %s vs. %s", c.pack, c.config, ret[c.pack])); 369 logDebug("Using configuration '%s' for %s", c.config, c.pack); 370 ret[c.pack] = c.config; 371 } 372 373 // check for conflicts (packages missing in the final configuration graph) 374 foreach (p; getTopologicalPackageList()) 375 enforce(p.name in ret, "Could not resolve configuration for package "~p.name); 376 377 return ret; 378 } 379 380 /** 381 * Fills dst with values from this project. 382 * 383 * dst gets initialized according to the given platform and config. 384 * 385 * Params: 386 * dst = The BuildSettings struct to fill with data. 387 * platform = The platform to retrieve the values for. 388 * config = Values of the given configuration will be retrieved. 389 * root_package = If non null, use it instead of the project's real root package. 390 * shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary. 391 */ 392 void addBuildSettings(ref BuildSettings dst, in BuildPlatform platform, string config, in Package root_package = null, bool shallow = false) 393 const { 394 auto configs = getPackageConfigs(platform, config); 395 396 foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) { 397 auto pkg_path = pkg.path.toNativeString(); 398 dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]); 399 400 assert(pkg.name in configs, "Missing configuration for "~pkg.name); 401 logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]); 402 403 auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]); 404 if (psettings.targetType != TargetType.none) { 405 if (shallow && pkg !is m_main) 406 psettings.sourceFiles = null; 407 processVars(dst, pkg_path, psettings); 408 if (psettings.importPaths.empty) 409 logWarn(`Package %s (configuration "%s") defines no import paths, use {"importPaths": [...]} or the default package directory structure to fix this.`, pkg.name, configs[pkg.name]); 410 if (psettings.mainSourceFile.empty && pkg is m_main && psettings.targetType == TargetType.executable) 411 logWarn(`Executable configuration "%s" of package %s defines no main source file, this may cause certain build modes to fail. Add an explicit "mainSourceFile" to the package description to fix this.`, configs[pkg.name], pkg.name); 412 } 413 if (pkg is m_main) { 414 if (!shallow) { 415 enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build."); 416 enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build."); 417 } 418 dst.targetType = psettings.targetType; 419 dst.targetPath = psettings.targetPath; 420 dst.targetName = psettings.targetName; 421 dst.workingDirectory = processVars(psettings.workingDirectory, pkg_path, true); 422 if (psettings.mainSourceFile.length) 423 dst.mainSourceFile = processVars(psettings.mainSourceFile, pkg_path, true); 424 } 425 } 426 427 // always add all version identifiers of all packages 428 foreach (pkg; this.getTopologicalPackageList(false, null, configs)) { 429 auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]); 430 dst.addVersions(psettings.versions); 431 } 432 } 433 434 void addBuildTypeSettings(ref BuildSettings dst, in BuildPlatform platform, string build_type) 435 { 436 bool usedefflags = !(dst.requirements & BuildRequirements.noDefaultFlags); 437 if (usedefflags) { 438 BuildSettings btsettings; 439 m_main.addBuildTypeSettings(btsettings, platform, build_type); 440 processVars(dst, m_main.path.toNativeString(), btsettings); 441 } 442 } 443 444 /// Determines if the given dependency is already indirectly referenced by other dependencies of pack. 445 bool isRedundantDependency(in Package pack, in Package dependency) 446 const { 447 foreach (dep; pack.dependencies.byKey) { 448 auto dp = getDependency(dep, true); 449 if (!dp) continue; 450 if (dp is dependency) continue; 451 foreach (ddp; getTopologicalPackageList(false, dp)) 452 if (ddp is dependency) return true; 453 } 454 return false; 455 } 456 457 458 /// Actions which can be performed to update the application. 459 Action[] determineActions(PackageSupplier[] packageSuppliers, UpdateOptions option) 460 { 461 scope(exit) writeDubJson(); 462 463 if(!m_main) { 464 Action[] a; 465 return a; 466 } 467 468 auto graph = new DependencyGraph(m_main); 469 if(!gatherMissingDependencies(packageSuppliers, graph) || graph.missing().length > 0) { 470 // Check the conflicts first. 471 auto conflicts = graph.conflicted(); 472 if(conflicts.length > 0) { 473 logError("The dependency graph could not be filled, there are conflicts."); 474 Action[] actions; 475 foreach( string pkg, dbp; graph.conflicted()) 476 actions ~= Action.conflict(pkg, dbp.dependency, dbp.packages); 477 478 // Missing dependencies could have some bogus results, therefore 479 // return only the conflicts. 480 return actions; 481 } 482 483 // Then check unresolved dependencies. 484 logError("The dependency graph could not be filled, there are unresolved dependencies."); 485 Action[] actions; 486 foreach( string pkg, rdp; graph.missing()) 487 actions ~= Action.failure(pkg, rdp.dependency, rdp.packages); 488 489 return actions; 490 } 491 492 // Gather retrieved 493 Package[string] retrieved; 494 retrieved[m_main.name] = m_main; 495 foreach(ref Package p; m_dependencies) { 496 auto pbase = p.basePackage; 497 auto pexist = retrieved.get(pbase.name, null); 498 if (pexist && pexist !is pbase){ 499 logError("Conflicting package references found:"); 500 logError(" %s %s: %s", pexist.name, pexist.vers, pexist.path.toNativeString()); 501 logError(" %s %s: %s", pbase.name, pbase.vers, pbase.path.toNativeString()); 502 throw new Exception("Conflicting package multi-references."); 503 } 504 retrieved[pbase.name] = pbase; 505 } 506 507 // Check against package list and add retrieval actions 508 Action[] actions; 509 void addAction(Action act) { 510 if (!actions.any!(a => a.type == act.type && a.location == act.location && a.packageId == act.packageId && a.vers == act.vers)) 511 actions ~= act; 512 } 513 int[string] upgradePackages; 514 foreach( string pkg, d; graph.needed() ) { 515 auto basepkg = pkg.getBasePackage(); 516 auto p = basepkg in retrieved; 517 // TODO: auto update to latest head revision 518 if(!p || (!d.dependency.matches(p.vers) && !d.dependency.matches(Version.MASTER))) { 519 if(!p) logDiagnostic("Triggering retrieval of required package '"~basepkg~"', which was not present."); 520 else logDiagnostic("Triggering retrieval of required package '"~basepkg~"', which doesn't match the required versionh. Required '%s', available '%s'.", d.dependency, p.vers); 521 addAction(Action.get(basepkg, PlacementLocation.userWide, d.dependency, d.packages)); 522 } else { 523 if (option & UpdateOptions.upgrade) { 524 auto existing = m_packageManager.getBestPackage(basepkg, d.dependency); 525 // Only add one upgrade action for each package. 526 if(basepkg !in upgradePackages && m_packageManager.isManagedPackage(existing)) { 527 logDiagnostic("Required package '"~basepkg~"' found with version '"~p.vers~"', upgrading."); 528 upgradePackages[basepkg] = 1; 529 addAction(Action.get(basepkg, PlacementLocation.userWide, d.dependency, d.packages)); 530 } 531 } 532 else { 533 logDiagnostic("Required package '"~basepkg~"' found with version '"~p.vers~"'"); 534 } 535 } 536 } 537 538 return actions; 539 } 540 541 /// Outputs a JSON description of the project, including its deoendencies. 542 void describe(ref Json dst, BuildPlatform platform, string config) 543 { 544 dst.mainPackage = m_main.name; 545 546 auto configs = getPackageConfigs(platform, config); 547 548 auto mp = Json.emptyObject; 549 m_main.describe(mp, platform, config); 550 dst.packages = Json([mp]); 551 552 foreach (dep; m_dependencies) { 553 auto dp = Json.emptyObject; 554 dep.describe(dp, platform, configs[dep.name]); 555 dst.packages = dst.packages.get!(Json[]) ~ dp; 556 } 557 } 558 559 private bool gatherMissingDependencies(PackageSupplier[] packageSuppliers, DependencyGraph graph) 560 { 561 RequestedDependency[string] missing = graph.missing(); 562 RequestedDependency[string] oldMissing; 563 while( missing.length > 0 ) { 564 logDebug("Try to resolve %s", missing.keys); 565 if( missing.keys == oldMissing.keys ){ // FIXME: should actually compare the complete AA here 566 bool different = false; 567 foreach(string pkg, reqDep; missing) { 568 auto o = pkg in oldMissing; 569 if(o && reqDep.dependency != o.dependency) { 570 different = true; 571 break; 572 } 573 } 574 if(!different) { 575 logWarn("Could not resolve dependencies"); 576 return false; 577 } 578 } 579 580 oldMissing = missing.dup; 581 logDebug("There are %s packages missing.", missing.length); 582 583 auto toLookup = missing; 584 foreach(id, dep; graph.optional()) { 585 assert(id !in toLookup, "A missing dependency in the graph seems to be optional, which is an error."); 586 toLookup[id] = dep; 587 } 588 589 foreach(string pkg, reqDep; toLookup) { 590 if(!reqDep.dependency.valid()) { 591 logDebug("Dependency to "~pkg~" is invalid. Trying to fix by modifying others."); 592 continue; 593 } 594 595 auto ppath = pkg.getSubPackagePath(); 596 597 // TODO: auto update and update interval by time 598 logDebug("Adding package to graph: "~pkg); 599 Package p = m_packageManager.getBestPackage(pkg, reqDep.dependency); 600 if( p ) logDebug("Found present package %s %s", pkg, p.ver); 601 602 // Don't bother with not available optional packages. 603 if( !p && reqDep.dependency.optional ) continue; 604 605 // Try an already present package first 606 if( p && needsUpToDateCheck(p) ){ 607 logInfo("Triggering update of package %s", pkg); 608 p = null; 609 } 610 611 if( !p ) p = fetchPackageMetadata(packageSuppliers, pkg, reqDep); 612 if( p ) graph.insert(p); 613 } 614 graph.clearUnused(); 615 616 // As the dependencies are filled in starting from the outermost 617 // packages, resolving those conflicts won't happen (?). 618 if(graph.conflicted().length > 0) { 619 logInfo("There are conflicts in the dependency graph."); 620 return false; 621 } 622 623 missing = graph.missing(); 624 } 625 626 return true; 627 } 628 629 private Package fetchPackageMetadata(PackageSupplier[] packageSuppliers, string pkg, RequestedDependency reqDep) { 630 Package p = null; 631 try { 632 logDiagnostic("Fetching package %s (%d suppliers registered)", pkg, packageSuppliers.length); 633 auto ppath = pkg.getSubPackagePath(); 634 auto basepkg = pkg.getBasePackage(); 635 foreach (supplier; packageSuppliers) { 636 try { 637 // Get main package. 638 auto sp = new Package(supplier.getPackageDescription(basepkg, reqDep.dependency, false)); 639 // Fetch subpackage, if one was requested. 640 foreach (spn; ppath[1 .. $]) { 641 try { 642 // Some subpackages are shipped with the main package. 643 sp = sp.getSubPackage(spn); 644 } catch (Exception e) { 645 // HACK: Support for external packages. Until the registry serves the 646 // metadata of the external packages, there is no way to get to 647 // know this information. 648 // 649 // Eventually, the dependencies of all referenced packages will be 650 // fulfilled, but this is done by a hack where the metadata of the 651 // external package is inferred as having no additional dependencies. 652 // When the package is then fetched the state is re-evaluated and 653 // possible new dependencies will be resolved. 654 string hacked_info = "{\"name\": \"" ~ spn ~ "\"}"; 655 auto info = parseJson(hacked_info); 656 sp = new Package(info, Path(), sp); 657 } 658 } 659 p = sp; 660 break; 661 } catch (Exception e) { 662 logDiagnostic("No metadata for %s: %s", supplier.description, e.msg); 663 } 664 } 665 enforce(p !is null, "Could not find package candidate for "~pkg~" "~reqDep.dependency.toString()); 666 markUpToDate(basepkg); 667 } 668 catch(Throwable e) { 669 logError("Failed to retrieve metadata for package %s: %s", pkg, e.msg); 670 logDiagnostic("Full error: %s", e.toString().sanitize()); 671 } 672 673 return p; 674 } 675 676 private bool needsUpToDateCheck(Package pack) { 677 version (none) { // needs to be updated for the new package system (where no project local packages exist) 678 try { 679 auto time = m_json["dub"]["lastUpdate"].opt!(Json[string]).get(pack.name, Json("")).get!string; 680 if( !time.length ) return true; 681 return (Clock.currTime() - SysTime.fromISOExtString(time)) > dur!"days"(1); 682 } catch(Exception t) return true; 683 } else return false; 684 } 685 686 private void markUpToDate(string packageId) { 687 logDebug("markUpToDate(%s)", packageId); 688 Json create(ref Json json, string object) { 689 if( object !in json ) json[object] = Json.emptyObject; 690 return json[object]; 691 } 692 create(m_json, "dub"); 693 create(m_json["dub"], "lastUpdate"); 694 m_json["dub"]["lastUpdate"][packageId] = Json( Clock.currTime().toISOExtString() ); 695 696 writeDubJson(); 697 } 698 699 private void writeDubJson() { 700 // don't bother to write an empty file 701 if( m_json.length == 0 ) return; 702 703 try { 704 logDebug("writeDubJson"); 705 auto dubpath = m_root~".dub"; 706 if( !exists(dubpath.toNativeString()) ) mkdir(dubpath.toNativeString()); 707 auto dstFile = openFile((dubpath~"dub.json").toString(), FileMode.CreateTrunc); 708 scope(exit) dstFile.close(); 709 dstFile.writePrettyJsonString(m_json); 710 } catch( Exception e ){ 711 logWarn("Could not write .dub/dub.json."); 712 } 713 } 714 } 715 716 /// Actions to be performed by the dub 717 struct Action { 718 enum Type { 719 fetch, 720 remove, 721 conflict, 722 failure 723 } 724 725 immutable { 726 Type type; 727 string packageId; 728 PlacementLocation location; 729 Dependency vers; 730 } 731 const Package pack; 732 const Dependency[string] issuer; 733 734 static Action get(string pkg, PlacementLocation location, in Dependency dep, Dependency[string] context) 735 { 736 return Action(Type.fetch, pkg, location, dep, context); 737 } 738 739 static Action remove(Package pkg, Dependency[string] context) 740 { 741 return Action(Type.remove, pkg, context); 742 } 743 744 static Action conflict(string pkg, in Dependency dep, Dependency[string] context) 745 { 746 return Action(Type.conflict, pkg, PlacementLocation.userWide, dep, context); 747 } 748 749 static Action failure(string pkg, in Dependency dep, Dependency[string] context) 750 { 751 return Action(Type.failure, pkg, PlacementLocation.userWide, dep, context); 752 } 753 754 private this(Type id, string pkg, PlacementLocation location, in Dependency d, Dependency[string] issue) 755 { 756 this.type = id; 757 this.packageId = pkg; 758 this.location = location; 759 this.vers = d; 760 this.issuer = issue; 761 } 762 763 private this(Type id, Package pkg, Dependency[string] issue) 764 { 765 pack = pkg; 766 type = id; 767 packageId = pkg.name; 768 vers = cast(immutable)Dependency(pkg.ver); 769 issuer = issue; 770 } 771 772 string toString() const { 773 return to!string(type) ~ ": " ~ packageId ~ ", " ~ to!string(vers); 774 } 775 } 776 777 778 enum UpdateOptions 779 { 780 none = 0, 781 upgrade = 1<<1, 782 preRelease = 1<<2 // inclde pre-release versions in upgrade 783 } 784 785 786 /// Indicates where a package has been or should be placed to. 787 enum PlacementLocation { 788 /// Packages retrived with 'local' will be placed in the current folder 789 /// using the package name as destination. 790 local, 791 /// Packages with 'userWide' will be placed in a folder accessible by 792 /// all of the applications from the current user. 793 userWide, 794 /// Packages retrieved with 'systemWide' will be placed in a shared folder, 795 /// which can be accessed by all users of the system. 796 systemWide 797 } 798 799 void processVars(ref BuildSettings dst, string project_path, BuildSettings settings, bool include_target_settings = false) 800 { 801 dst.addDFlags(processVars(project_path, settings.dflags)); 802 dst.addLFlags(processVars(project_path, settings.lflags)); 803 dst.addLibs(processVars(project_path, settings.libs)); 804 dst.addSourceFiles(processVars(project_path, settings.sourceFiles, true)); 805 dst.addImportFiles(processVars(project_path, settings.importFiles, true)); 806 dst.addStringImportFiles(processVars(project_path, settings.stringImportFiles, true)); 807 dst.addCopyFiles(processVars(project_path, settings.copyFiles, true)); 808 dst.addVersions(processVars(project_path, settings.versions)); 809 dst.addDebugVersions(processVars(project_path, settings.debugVersions)); 810 dst.addImportPaths(processVars(project_path, settings.importPaths, true)); 811 dst.addStringImportPaths(processVars(project_path, settings.stringImportPaths, true)); 812 dst.addPreGenerateCommands(processVars(project_path, settings.preGenerateCommands)); 813 dst.addPostGenerateCommands(processVars(project_path, settings.postGenerateCommands)); 814 dst.addPreBuildCommands(processVars(project_path, settings.preBuildCommands)); 815 dst.addPostBuildCommands(processVars(project_path, settings.postBuildCommands)); 816 dst.addRequirements(settings.requirements); 817 dst.addOptions(settings.options); 818 819 if (include_target_settings) { 820 dst.targetType = settings.targetType; 821 dst.targetPath = processVars(settings.targetPath, project_path, true); 822 dst.targetName = settings.targetName; 823 dst.workingDirectory = processVars(settings.workingDirectory, project_path, true); 824 if (settings.mainSourceFile.length) 825 dst.mainSourceFile = processVars(settings.mainSourceFile, project_path, true); 826 } 827 } 828 829 private string[] processVars(string project_path, string[] vars, bool are_paths = false) 830 { 831 auto ret = appender!(string[])(); 832 processVars(ret, project_path, vars, are_paths); 833 return ret.data; 834 835 } 836 private void processVars(ref Appender!(string[]) dst, string project_path, string[] vars, bool are_paths = false) 837 { 838 foreach (var; vars) dst.put(processVars(var, project_path, are_paths)); 839 } 840 841 private string processVars(string var, string project_path, bool is_path) 842 { 843 auto idx = std..string.indexOf(var, '$'); 844 if (idx >= 0) { 845 auto vres = appender!string(); 846 while (idx >= 0) { 847 if (idx+1 >= var.length) break; 848 if (var[idx+1] == '$') { 849 vres.put(var[0 .. idx+1]); 850 var = var[idx+2 .. $]; 851 } else { 852 vres.put(var[0 .. idx]); 853 var = var[idx+1 .. $]; 854 855 size_t idx2 = 0; 856 while( idx2 < var.length && isIdentChar(var[idx2]) ) idx2++; 857 auto varname = var[0 .. idx2]; 858 var = var[idx2 .. $]; 859 860 string env_variable; 861 if( varname == "PACKAGE_DIR" ) vres.put(project_path); 862 else if( (env_variable = environment.get(varname)) != null) vres.put(env_variable); 863 else enforce(false, "Invalid variable: "~varname); 864 } 865 idx = std..string.indexOf(var, '$'); 866 } 867 vres.put(var); 868 var = vres.data; 869 } 870 if (is_path) { 871 auto p = Path(var); 872 if (!p.absolute) { 873 logDebug("Fixing relative path: %s ~ %s", project_path, p.toNativeString()); 874 p = Path(project_path) ~ p; 875 } 876 return p.toNativeString(); 877 } else return var; 878 } 879 880 private bool isIdentChar(dchar ch) 881 { 882 return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '_'; 883 } 884 885 string stripDlangSpecialChars(string s) 886 { 887 import std.array; 888 import std.uni; 889 auto ret = appender!string(); 890 foreach(ch; s) 891 ret.put(isIdentChar(ch) ? ch : '_'); 892 return ret.data; 893 }